Java Threads

Reference: JavaGuide

Threads

Thread Implementation

A Java program runs as a process and is actually multi-threaded concurrently; the main function is the main thread. Each instance of java.lang.Thread that has called start() and not yet finished is a thread. Java threads are the concurrent execution units of Java programs.

One-to-One Thread Model✨

Each Java thread corresponds to a kernel thread in the operating system, fully utilizing multi-core processors. (Now Windows and Linux use this.)

JVM implements Java thread creation and synchronization by calling the thread library provided by the OS; Java thread scheduling and management are handled by the OS.

Many-to-One Thread Model

What are user threads? They are threads fully implemented in user space; the OS kernel is unaware of them. User thread creation, synchronization, destruction, and scheduling are all done in user mode without kernel help. If implemented well, these threads don’t need to switch to kernel mode, so operations are fast and low-cost, supporting larger numbers of threads. Some high-performance databases use user threads for multi-threading.

Multiple user threads map to one kernel thread. Thread switching is very fast and can support many user threads. But cannot utilize multi-core processors; blocking operations can hang the entire process.

Java used this early on.

Many-to-Many Thread Model

Multiple user threads map to a few kernel threads. Implementation is complex. Goroutines use this model.

Creating Threads

Strictly speaking, the only way to create a new thread in Java is the start() method.

Inherit Thread Class

Override the run() method of Thread class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class MyThread extends Thread {
    @Override
    public void run() {
        // Thread task
        
    }
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        thread1.start();
    }
}

Implement Runnable Interface

Pass the implemented class object as parameter to Thread constructor.

1
2
3
4
5
6
7
public class MyRunnableLambda {
    public static void main(String[] args) {
        Runnable runnable = () -> {System.out.println("hi");};
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

Runnable interface is more flexible, allows better resource sharing (multiple threads can share one Runnable object), and works better with thread pool APIs.

FutureTask

Can return task execution result.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class MyFutureTask {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(()->{
            System.out.println("hi");
            return 1;
        });
        new Thread(futureTask, "task1").start();
        Integer result=futureTask.get();
        System.out.println(result);
    }
}

Can I call Thread’s run() directly?

If you create a thread and call thread.run() in main, it’s still the main thread executing run()’s logic.

Calling thread.start() starts a new thread, puts it in RUNNABLE state, and hands run()’s logic to the new thread.

Thread Lifecycle and States

Java threads have six states.

  1. NEW, initial state after creation
  2. RUNNABLE, state after start(), includes OS thread READY and RUNNING. Either waiting for OS scheduling or running.
  3. WAITING, thread won’t be scheduled until explicitly woken by another thread.
  4. TIMED_WAITING, thread won’t be scheduled, woken by system after time.
  5. BLOCKED, blocked without lock, until another thread releases lock; JVM randomly picks or sequentially chooses a BLOCKED thread for the lock.
  6. TERMINATED, thread has finished execution. Alt text

Why not distinguish READY and RUNNING? Because time slices switch quickly; a thread might execute briefly then schedule to others.

sleep() and wait()

sleep()

Thread.sleep(2000); // Sleep thread for 2 seconds

sleep() is Thread’s static native method, changes thread from RUNNABLE to TIMED_WAITING. Does not release lock.

wait()

wait() is Object’s instance native method, must be called in synchronized block/method, releases lock.

1
2
3
4
5
6
synchronized (obj) {
    while (condition not met) {  // Must loop to check condition, prevent spurious wakeup
        obj.wait();      // Release lock, enter wait queue
    }
    // Operations after condition met
}

wait() changes thread from RUNNABLE to WAITING, enters object’s waiting set.
Another thread on same lock calls lock.notify(), randomly wakes one from waiting set, adds to entry set (sync queue), state from (TIMED) WAIT to BLOCKED.

Sync queue: When thread wants synchronized lock but doesn’t get it, adds to lock’s sync queue, state BLOCKED. When lock released, JVM randomly (unfair) chooses thread to grant lock.

wait() must be instance method because releasing lock operates on the lock object.

Multi-Threading

Java Thread Scheduling

Scheduling types: Cooperative and Preemptive.

Cooperative: Thread controls its execution time. After work, notifies system to switch. Advantage: No sync issues. Disadvantage: One thread running too long blocks others.

Preemptive: System controls execution time.

Java provides 10 thread priorities; if two RUNNABLE, higher priority scheduled first. Java threads map one-to-one to kernel threads, Java priorities partially correspond to kernel priorities.

Single-Core CPU Running Multi-Threads

Threads are CPU-intensive or IO-intensive. For CPU-intensive, frequent context switches reduce efficiency; for IO-intensive, switching during IO wait allows others to work, reasonable.

Deadlock

Deadlock conditions: mutual exclusion, hold and wait, no preemption, circular wait

1
cd /Users/ruoke/Library/Java/JavaVirtualMachines/corretto-17.0.13/Contents/Home/bin

Run jconsole, select process, view threads Alt text