Post

Java Threads (Java the Definitive Guide)

Java Threads (Java the Definitive Guide)

This post was migrated from Tistory. You can find the original here.

Notes from reading Java the Definitive Guide (Nam-gung Seong).

Thread

On a single core, running a single task with multiple threads actually takes longer than running it with a single thread.

The reason is that it’s just a sequential alternation of th1 -> th2 -> th1 -> th2, which only adds extra context switching overhead.

On multiple cores, multithreading actually helps, since th1 and th2 can run concurrently.

(This assumes one core runs one thread at a time.)

(Don’t confuse “thread” here with the threads in a Tomcat thread pool.)


Java runs on the JVM and is said to be OS-independent, but there are actually a few OS-dependent aspects, and threads are one of them.

Just as the JVM’s thread scheduler decides which thread runs and for how long, a process’s order and execution time are determined by the OS’s process scheduler. Since the time allotted to a process varies depending on the situation at any given moment, the time allotted to its threads is likewise not fixed.


For tasks that use different resources, multithreading is efficient.

A thread waiting to handle I/O is called I/O blocking.


Java lets you set thread priorities, but on multi-core systems you can’t expect consistent (meaningful) results from this,

because Java doesn’t enforce how threads with different priorities should be treated differently, so the implementation related to thread priority can vary between JVMs.


Threads can be managed as groups. You can change your own group and its subgroups, but not the threads of other groups.

Every thread must belong to a thread group. A thread created without specifying a group is, by default, placed in the same group as the thread that created it.

When a Java application runs, the JVM creates thread groups called main and system, and places the threads it needs for its own operation into these groups. For example, the Finalizer thread that performs GC belongs to the system group. All thread groups we create become subgroups of the main thread group, and a thread created without specifying a group automatically belongs to the main thread group.


From the moment a thread is created until it terminates, it can be in one of the following states:

  • NEW: Created, but start() has not been called yet
  • RUNNABLE: Running or able to run
  • BLOCKED: Paused by a synchronized block (waiting to acquire a lock)
  • WAITING, TIME_WAITING: The task hasn’t finished but the thread isn’t runnable (what else besides a timed pause could this be?); TIME_WAITING refers to the case where a pause duration is specified
  • TERMINATED: The thread’s work has finished


1
2
3
try {
    th1.sleep(1000);
} catch (InterruptedException e) {}

sleep() always operates on the currently running thread, so even if you call th1.sleep, it actually applies to the main (current) thread.

1
2
3
4
5
6
7
8
9
th.interrupt();

class MyThread extends Thread {
    public void run() {
        while(!interrupted()) {
            ....
        }
    }
}

interrupt doesn’t forcibly terminate a thread’s work. It just changes the thread’s interrupted status.

So once interrupt has been called, interrupted() returns true.

join() - waits for another thread’s task to finish.

yield() - yields to another thread.


For tasks that use the same resource, if you’re using multiple threads, you need to consider thread synchronization.

Preventing one thread from interfering with a task that another thread is currently performing is called “thread synchronization.”

There are the concepts of the critical section and the lock.

Let’s look at the simplest synchronization method, the synchronized keyword. (There are also various other locks, atomics, etc.)

1
2
3
4
5
6
7
8
9
// Designate the entire method as a critical section
public synchronized void test() {
    ....
}
    
// Designate a specific block as a critical section
synchronized(reference variable to an object) {
    ....
}

There are the two approaches shown above. The reference variable here must refer to the object you want to lock.

With both approaches, acquiring and releasing the lock happen automatically.

Every object holds exactly one lock.

Whether you designate a method or a block, the lock is placed on the object. (It is not placed on the method or the block itself.)

While synchronized protects shared data, it’s also important that a particular thread doesn’t hold an object’s lock for too long.

That’s why we use wait() and notify().

th1 calls wait() to release the lock and waits at that point; once th2, which acquired the lock, finishes its work and calls notify(), th1 resumes its work.

The problem with notify() is that when multiple threads are waiting via wait(), there’s no guarantee that the thread that has waited longest will get the lock.

You can use notifyAll() to notify all threads, but there’s still only one lock.

A waiting pool exists per object.

Calling notifyAll() doesn’t wake up the threads in every object’s waiting pool.

Only the threads waiting in the waiting pool of the object on which notifyAll() was called are affected.

This post is licensed under CC BY-NC 4.0 by the author.