Introduction
So, we know that Java has threads. You can read about that in the review entitled Better together: Java and the Thread class. Part I — Threads of execution. Threads are necessary to perform work in parallel. This makes it highly likely that the threads will somehow interact with one another. Let's look at how this happens and what basic tools we have.![Better together: Java and the Thread class. Part II — Synchronization - 1](https://cdn.codegym.cc/images/article/8ff41897-b282-41cc-8431-850cef1dcb82/800.jpeg)
Yield
Thread.yield() is baffling and rarely used. It is described in many different ways on the Internet. Including some people writing that there is some queue of threads, in which a thread will descend based on thread priorities. Other people write that a thread will change its status from "Running" to "Runnable" (even though there is no distinction between these statuses, i.e. Java does not distinguish between them). The reality is that it's all much less well-known and yet simpler in a sense.![Better together: Java and the Thread class. Part II — Synchronization - 2](https://cdn.codegym.cc/images/article/3959a540-1de0-4152-8c65-56e54084864e/800.jpeg)
yield()
method's documentation. If you read it, it is clear that the yield()
method actually only provides some recommendation to the Java thread scheduler that this thread can be given less execution time. But what actually happens, i.e. whether the scheduler acts on the recommendation and what it does in general, depends on the JVM's implementation and the operating system. And it may depend on some other factors as well. All the confusion is most likely due to the fact that multithreading has been rethought as the Java language has developed.
Read more in the overview here: Brief Introduction to Java Thread.yield().
Sleep
A thread can go to sleep during its execution. This is the easiest type of interaction with other threads. The operating system that runs the Java virtual machine that runs our Java code has its own thread scheduler. It decides which thread to start and when. A programmer cannot interact with this scheduler directly from Java code, only through the JVM. He or she can ask the scheduler to pause the thread for a while, i.e. to put it to sleep. You can read more in these articles: Thread.sleep() and How Multithreading works. You can also check out how threads work in Windows operating systems: Internals of Windows Thread. And now let's see it with our own eyes. Save the following code in a file namedHelloWorldApp.java
:
class HelloWorldApp {
public static void main(String []args) {
Runnable task = () -> {
try {
int secToWait = 1000 * 60;
Thread.currentThread().sleep(secToWait);
System.out.println("Woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(task);
thread.start();
}
}
As you can see, we have some task that waits for 60 seconds, after which the program ends.
We compile using the command "javac HelloWorldApp.java
" and then run the program using "java HelloWorldApp
".
It's best to start the program in a separate window. For example, on Windows, it is like this: start java HelloWorldApp
.
We use the jps command to get the PID (process ID), and we open the list of threads with "jvisualvm --openpid pid
:
![Better together: Java and the Thread class. Part II — Synchronization - 3](https://cdn.codegym.cc/images/article/4b17ebd6-9a1e-480f-89fc-d452b946ddf5/800.jpeg)
try {
TimeUnit.SECONDS.sleep(60);
System.out.println("Woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
Did you notice that we're handling InterruptedException
everywhere? Let's understand why.
Thread.interrupt()
The thing is that while a thread is waiting/sleeping, someone may want to interrupt. In this case, we handle anInterruptedException
. This mechanism was created after the Thread.stop()
method was declared Deprecated, i.e. outdated and undesirable. The reason was that when the stop()
method was called, the thread was simply "killed", which was very unpredictable. We could not know when the thread would be stopped, and we could not guarantee data consistency.
Imagine that you're writing data to a file while the thread is killed. Rather than killing the thread, Java's creators decided that it would be more logical to tell it that it should be interrupted. How to respond to this information is a matter for the thread itself to decide. For more details, read Why is Thread.stop deprecated? on Oracle's website. Let's look at an example:
public static void main(String []args) {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(60);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
In this example, we won't wait 60 seconds. Instead, we'll immediately display "Interrupted". This is because we called the interrupt()
method on the thread. This method sets an internal flag called "interrupt status". That is, each thread has an internal flag that is not directly accessible. But we have native methods for interacting with this flag.
But that isn't the only way. A thread may be running, not waiting for something, simply performing actions. But it may anticipate that others will want to end its work at a specific time. For example:
public static void main(String []args) {
Runnable task = () -> {
while(!Thread.currentThread().isInterrupted()) {
// Do some work
}
System.out.println("Finished");
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
In the example above, the while
loop will be executed until the thread is interrupted externally.
As for the isInterrupted
flag, it is important to know that if we catch an InterruptedException
, the isInterrupted flag gets reset, and then isInterrupted()
will return false. The Thread class also has a static Thread.interrupted() method that applies only to the current thread, but this method resets the flag to false!
Read more in this chapter entitled Thread Interruption.
Join (Wait for another thread to finish)
The simplest type of waiting is waiting for another thread to finish.public static void main(String []args) throws InterruptedException {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.join();
System.out.println("Finished");
}
In this example, the new thread will sleep 5 seconds. At the same time, the main thread will wait until the sleeping thread wakes up and finishes its work. If you look at the thread's state in JVisualVM, then it will look like this:
![Better together: Java and the Thread class. Part II — Synchronization - 4](https://cdn.codegym.cc/images/article/3d7d682c-18f0-481e-a205-3205776fabb3/800.jpeg)
join
method is pretty simple, because it is just a method with Java code that executes wait()
as long as the thread on which it is called is alive. As soon as the thread dies (when it is finished with its work), the wait is interrupted. And that's all the magic of the join()
method. So, let's move on to the most interesting thing.
Monitor
Multithreading includes the concept of a monitor. The word monitor comes to English by way of 16th-century Latin and means "an instrument or device used for observing, checking, or keeping a continuous record of a process". In the context of this article, we will try to cover the basics. For anyone who wants the details, please dive into the linked materials. We begin our journey with the Java Language Specification (JLS): 17.1. Synchronization. It says the following:![Better together: Java and the Thread class. Part II — Synchronization - 5](https://cdn.codegym.cc/images/article/601d44a1-4581-43b8-89b7-50c818ddd8ac/1024.jpeg)
lock()
or release it with unlock()
.
Next, we'll find the tutorial on the Oracle website: Intrinsic Locks and Synchronization.
This tutorial says that Java's synchronization is built around an internal entity called an intrinsic lock or monitor lock. This lock is often simply called a "monitor". We also see again that every object in Java has an intrinsic lock associated with it.
You can read Java - Intrinsic Locks and Synchronization.
Next it will be important to understand how an object in Java can be associated with a monitor. In Java, each object has a header which stores internal metadata not available to the programmer from the code, but which the virtual machine needs to correctly work with objects.
The object header includes a "mark word", which looks like this:
![Better together: Java and the Thread class. Part II — Synchronization - 6](https://cdn.codegym.cc/images/article/b710063c-14d9-4589-8ec0-db29cbe7ed99/512.jpeg)
https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf
public class HelloWorld{
public static void main(String []args){
Object object = new Object();
synchronized(object) {
System.out.println("Hello World");
}
}
}
Here, the current thread (the one on which these lines of code are executed) uses the synchronized
keyword to attempt to use the monitor associated with the object"\
variable to get/acquire the lock. If nobody else is contending for the monitor (i.e. no one else is running synchronized code using the same object), then Java may try to perform an optimization called "biased locking". A relevant tag and a record about which thread owns the monitor's lock are added to the mark word in the object header. This reduces the overhead required to lock a monitor.
If the monitor was previously owned by another thread, then such locking is not enough. The JVM switches to the next type of locking: "basic locking". It uses compare-and-swap (CAS) operations. What's more, the object header's mark word itself no longer stores the mark word, but rather a reference to where it is stored, and the tag changes so that the JVM understands that we are using basic locking.
If multiple threads compete (contend) for a monitor (one has acquired the lock, and a second is waiting for the lock to be released), then the tag in the mark word changes, and the mark word now store a reference to the monitor as an object — some internal entity of the JVM. As stated in the JDK Enchancement Proposal (JEP), this situation requires space in the Native Heap area of memory in order to store this entity. The reference to this internal entity's memory location will be stored in the object header's mark word.
Thus, a monitor is really a mechanism for synchronizing access to shared resources among multiple threads. The JVM switches between several implementations of this mechanism. So, for simplicity, when talking about the monitor, we're actually talking about locks.
![Better together: Java and the Thread class. Part II — Synchronization - 7](https://cdn.codegym.cc/images/article/16d7cedb-ef29-4828-9a44-78ef3433e559/800.jpeg)
Synchronized (waiting for a lock)
As we saw earlier, the concept of a "synchronized block" (or "critical section") is closely related to the concept of a monitor. Take a look at an example:public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Runnable task = () -> {
synchronized(lock) {
System.out.println("thread");
}
};
Thread th1 = new Thread(task);
th1.start();
synchronized(lock) {
for (int i = 0; i < 8; i++) {
Thread.currentThread().sleep(1000);
System.out.print(" " + i);
}
System.out.println(" ...");
}
}
Here, the main thread first passes the task object to the new thread, and then immediately acquires the lock and performs a long operation with it (8 seconds). All this time, the task is unable to proceed, because it cannot enter the synchronized
block, because the lock is already acquired.
If the thread cannot get the lock, it will wait for the monitor. As soon as it gets the lock, it will continue execution. When a thread exits a monitor, it releases the lock. In JVisualVM, it looks like this:
![Better together: Java and the Thread class. Part II — Synchronization - 8](https://cdn.codegym.cc/images/article/93ab7c13-e090-46d8-b343-16054436f6e2/800.jpeg)
th1.getState()
statement in the for loop will return BLOCKED, because as long as the loop is running, the lock
object's monitor is occupied by the main
thread, and the th1
thread is blocked and cannot proceed until the lock is released. In addition to synchronized blocks, an entire method can be synchronized.
For example, here's a method from the HashTable
class:
public synchronized int size() {
return count;
}
This method will be executed by only one thread at any given time. Do we really need the lock? Yes, we need it. In the case of instance methods, the "this" object (current object) acts as a lock. There is an interesting discussion on this topic here: Is there an advantage to using a Synchronized Method instead of a Synchronized Block?.
If the method is static, then the lock will not be the "this" object (because there is no "this" object for a static method), but rather a Class object (for example, Integer.class
).
Wait (waiting for a monitor). notify() and notifyAll() methods
The Thread class has another waiting method that is associated with a monitor. Unlikesleep()
and join()
, this method can't simply be called. Its name is wait()
.
The wait
method is called on the object associated with the monitor that we want to wait for.
Let's see an example:
public static void main(String []args) throws InterruptedException {
Object lock = new Object();
// The task object will wait until it is notified via lock
Runnable task = () -> {
synchronized(lock) {
try {
lock.wait();
} catch(InterruptedException e) {
System.out.println("interrupted");
}
}
// After we are notified, we will wait until we can acquire the lock
System.out.println("thread");
};
Thread taskThread = new Thread(task);
taskThread.start();
// We sleep. Then we acquire the lock, notify, and release the lock
Thread.currentThread().sleep(3000);
System.out.println("main");
synchronized(lock) {
lock.notify();
}
}
In JVisualVM, it looks like this:
![Better together: Java and the Thread class. Part II — Synchronization - 10](https://cdn.codegym.cc/images/article/2245dec1-e120-4a9a-a274-a760fb7f7bf3/800.jpeg)
wait()
and notify()
methods are associated with java.lang.Object
. It may seem strange that thread-related methods are in the Object
class. But the reason for that now unfolds.
You will recall that every object in Java has a header. The header contains various housekeeping information, including information about the monitor, i.e. the status of the lock. Remember, each object, or instance of a class, is associated with an internal entity in the JVM, called an intrinsic lock or monitor.
In the example above, the code for the task object indicates that we enter the synchronized block for the monitor associated with the lock
object. If we succeed in acquiring the lock for this monitor, then wait()
is called. The thread executing the task will release the lock
object's monitor, but will enter the queue of threads waiting for notification from the lock
object's monitor. This queue of threads is called a WAIT SET, which more properly reflects its purpose. That is, it's more of a set than a queue.
The main
thread creates a new thread with the task object, starts it, and waits 3 seconds. This makes it highly likely that the new thread will be able to acquire the lock before the main
thread, and get into the monitor's queue. After that, the main
thread itself enters the lock
object's synchronized block and performs thread notification using the monitor. After the notification is sent, the main
thread releases the lock
object's monitor, and the new thread, which was previously waiting for the lock
object's monitor to be released, continues execution.
It is possible to send a notification to only one thread (notify()
) or simultaneously to all threads in the queue (notifyAll()
).
Read more here: Difference between notify() and notifyAll() in Java. It's important to note that the notification order depends on how the JVM is implemented.
Read more here: How to solve starvation with notify and notifyAll?.
Synchronization can be performed without specifying an object. You can do this when an entire method is synchronized rather than a single block of code.
For example, for static methods, the lock will be a Class object (obtained via .class
):
public static synchronized void printA() {
System.out.println("A");
}
public static void printB() {
synchronized(HelloWorld.class) {
System.out.println("B");
}
}
In terms of using locks, both methods are the same.
If a method is not static, then synchronization will be performed using the current instance
, that is, using this
.
By the way, we said earlier you can use the getState()
method to get the status of a thread.
For example, for a thread in the queue waiting for a monitor, the status will be WAITING or TIMED_WAITING, if the wait()
method specified a timeout.
![Better together: Java and the Thread class. Part II — Synchronization - 11](https://cdn.codegym.cc/images/article/6dc08067-eb01-42a8-a091-5290b2582a21/512.jpeg)
https://stackoverflow.com/questions/36425942/what-is-the-lifecycle-of-thread-in-java
Thread life cycle
Over the course of its life, a thread's status changes. In fact, these changes comprise the thread's life cycle. As soon as a thread is created, its status is NEW. In this state, the new thread is not yet running and the Java thread scheduler does not yet know anything about it. In order for the thread scheduler to learn about the thread, you must call thethread.start()
method. Then the thread will transition to the RUNNABLE state. The Internet has lots of incorrect diagrams that distinguish between "Runnable" and "Running" states. But this is a mistake, because Java does not distinguish between "ready to work" (runnable) and "working" (running).
When a thread is alive but not active (not Runnable), it is in one of two states:
- BLOCKED — waiting to enter a critical section, i.e. a
synchronized
block. - WAITING — waiting for another thread to satisfy some condition.
getState()
method.
Threads also have an isAlive()
method, which returns true if the thread is not TERMINATED.
LockSupport and thread parking
Beginning with Java 1.6, an interesting mechanism called LockSupport appeared.![Better together: Java and the Thread class. Part II — Synchronization - 12](https://cdn.codegym.cc/images/article/345e2fbe-17f2-470d-89a2-a82a711136b8/800.jpeg)
park()
method returns immediately if the permit is available, consuming the permit in the process. Otherwise, it blocks.
Calling the unpark
method makes the permit available if it is not yet available.
There is only 1 permit.
The Java documentation for LockSupport
refers to the Semaphore
class. Let's look at a simple example:
import java.util.concurrent.Semaphore;
public class HelloWorldApp{
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(0);
try {
semaphore.acquire();
} catch (InterruptedException e) {
// Request the permit and wait until we get it
e.printStackTrace();
}
System.out.println("Hello, World!");
}
}
This code will always wait, because now the semaphore has 0 permits. And when acquire()
is called in the code (i.e. request the permit), the thread waits until it receives the permit.
Since we are waiting, we must handle InterruptedException
.
Interestingly, the semaphore gets a separate thread state. If we look in JVisualVM, we will see that the state is not "Wait", but "Park".
![Better together: Java and the Thread class. Part II — Synchronization - 13](https://cdn.codegym.cc/images/article/937f0e6b-f11a-4bb8-a965-734d52d198e4/800.jpeg)
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
// Park the current thread
System.err.println("Will be Parked");
LockSupport.park();
// As soon as we are unparked, we will start to act
System.err.println("Unparked");
};
Thread th = new Thread(task);
th.start();
Thread.currentThread().sleep(2000);
System.err.println("Thread state: " + th.getState());
LockSupport.unpark(th);
Thread.currentThread().sleep(2000);
}
The thread's status will be WAITING, but JVisualVM distinguishes between wait
from the synchronized
keyword and park
from the LockSupport
class.
Why is this LockSupport
so important? We turn again to the Java documentation and look at the WAITING thread state.
As you can see, there are only three ways to get into it. Two of those ways are wait()
and join()
. And the third is LockSupport
.
In Java, locks can also be built on LockSuppor
t and offer higher-level tools. Let's try to use one.
For example, take a look at ReentrantLock
:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HelloWorld{
public static void main(String []args) throws InterruptedException {
Lock lock = new ReentrantLock();
Runnable task = () -> {
lock.lock();
System.out.println("Thread");
lock.unlock();
};
lock.lock();
Thread th = new Thread(task);
th.start();
System.out.println("main");
Thread.currentThread().sleep(2000);
lock.unlock();
}
}
Just as in the previous examples, everything is simple here. The lock
object waits for someone to release the shared resource. If we look in JVisualVM, we'll see that the new thread will be parked until the main
thread releases the lock to it.
You can read more about locks here: Java 8 StampedLocks vs. ReadWriteLocks and Synchronized and Lock API in Java.
To better understand how locks are implemented, it's helpful to read about Phaser in this article: Guide to the Java Phaser. And speaking about various synchronizers, you must read the DZone article on The Java Synchronizers.
Conclusion
In this review, we examined the main ways that threads interact in Java. Additional material:- Better together: Java and the Thread class. Part I — Threads of execution
- https://dzone.com/articles/the-java-synchronizers
- https://www.javatpoint.com/java-multithreading-interview-questions