Java provides many ways to create and write multithreaded code.
There are 2 main types of threads in Java:
Platform Threads are native OS threads that Java wraps to provide a simple and consistent API for us to use (Java’s whole thing is write once run everywhere!). Comes with the resource expense of OS threads, but are great for CPU bound tasks.
Virtual Threads are ‘threads’ fully managed by the JVM, are extremely lightweight and good for I/O bound tasks, they share the same API as Platform Threads.
Java Thread States
Since Java provides a higher level API to interact with threads. It has it’s own lifecycle for threads and their state!
This also means that both Platform & Virtual Threads have the same API, making our multithreaded code agnostic to the actual underlying implementation of the threads, be it Platform or Virtual.
Image from baeldung.com on thread lifecycle
A thread can be in 1 of 7 states at any given time. 2 of which are just when it is created and when it is terminated. So that leaves us with 5 interesting states.\
- Runnable
- Ready - Ready to be executed, waiting for its time on the CPU
- Running - Currently executing
- Waiting - Waits for some other thread to notify it to wake up and become runnable again
- Timed Waiting - Same as waiting, but it won’t wait forever, becoming runnable after a set amount of time
- Blocked - Waiting for control of a monitor
Multithreading Primitives and Constructs
Like primitive and construct variables, Java has many multithreading primitives and constructs to help us easily write multithreaded applications!
Here is brief overview of a few:
- Monitors & synchronized
- Locks
- Volatile
- Executor Service & Future
Monitors & Synchronized
Monitors are a multithreading construct also known as intrinsic locks as they are an implementation of a mutex lock that exists inside every object.
I say “exists inside every object”, but a monitor is only created when required.
At runtime, every object has a header that stores metadata about itself. In this header, there is a section known as the “Mark Word”, the last two bits in the section are used to track if a thread has locked the object.
If only 1 thread has locked the object, no monitor is created, and the last two bits are set to 01
, denoting that the object is locked.
However, if another thread comes along and contends for the object while it is locked, the last two bits become 10
and a monitor is created to manage the threads contending for the object!
Monitors are used with the synchronized
keyword.
class Main {
public static void main(String[] args) {
ThreadSafeCounter counter = new ThreadSafeCounter();
// This thread calls this method on an instance of ThreadSafeCounter,
// Thread acquires the lock for counter. (monitor is not created yet)
new Thread(()-> counter.incr()).start();
// This thread then calls a method on the same instance,
// while the first thread is still running the incr() method.
// It sees that the object is currently locked and
// this thread is put into a blocked state until the first thread is done. (monitor is created)
new Thread(()-> counter.get()).start();
}
}
class ThreadSafeCounter {
private int count = 0;
public synchronized int get(){
return count;
}
public synchronized void incr() {
// artificially delay this method
try { Thread.sleep(1000); } catch(InterruptedException _e) {}
count++;
}
}
Links to more in-depth notes on monitors and synchronize.