Introduction
So, we know that Java has threads, as you can read about in the overview " Threads Don't Screw Java: Part I - Threads ". Threads are needed to do work at the same time. Therefore, it is very likely that the threads will somehow interact with each other. Let's see how this happens and what basic controls we have.Yield
The Thread.yield() method is cryptic and rarely used. There are many variations of his description on the Internet. Up to the point that some write about some kind of queue of threads in which the thread will move down, taking into account their priorities. Someone writes that the thread will change the status from running to runnable (although there is no separation between these statuses, and Java does not distinguish between them). But in fact, everything is much more unknown and in a sense simpler. There is a bug " JDK-6416721 : (spec thread) Fix Thread.yield() javadoc " on the topic of method documentation . If you read it, it is clear that in fact the methodyield
yield
just conveys some recommendation to the Java thread scheduler that the given thread can be given less execution time. But what will actually happen, whether the scheduler will hear the recommendation and what it will do in general, depends on the implementation of the JVM and the operating system. Or maybe some other factors. All the confusion has developed, most likely, due to the rethinking of multithreading in the development of the Java language. You can read more in the overview " Brief Introduction to Java Thread.yield() ".
Sleep - Falling asleep thread
A thread may fall asleep while it is executing. This is the simplest type of interaction with other threads. The operating system on which the Java virtual machine is installed, where Java code is running, has its own thread scheduler called Thread Scheduler. It is he who decides which thread to run when. The programmer cannot interact with this scheduler directly from the Java code, but he can ask the scheduler through the JVM to pause the thread for a while, "put" it to sleep. For details, see the articles " Thread.sleep() " and " How Multithreading works ". Moreover, you can learn how threads are arranged in Windows OS: " Internals of Windows Thread ". And now we will see it with our own eyes. SaveHelloWorldApp.java
the following code to a file:
class HelloWorldApp {
public static void main(String []args) {
Runnable task = () -> {
try {
int secToWait = 1000 * 60;
Thread.currentThread().sleep(secToWait);
System.out.println("Waked up");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(task);
thread.start();
}
}
As you can see, we have some task (task) in which a wait of 60 seconds is performed, after which the program ends. Let's compile javac HelloWorldApp.java
and run java HelloWorldApp
. It is better to run in a separate window. For example, on Windows it would be: start java HelloWorldApp
. Using the jps command, we find out the PID of the process and open the list of threads with jvisualvm --openpid pidПроцесса
: As you can see, our thread has switched to the Sleeping status. In fact, sleeping the current thread can be done more beautifully:
try {
TimeUnit.SECONDS.sleep(60);
System.out.println("Waked up");
} catch (InterruptedException e) {
e.printStackTrace();
}
You probably noticed that we process everywhere InterruptedException
? Let's understand why.
Thread interrupt or Thread.interrupt
The thing is that while a thread is waiting in a dream, someone may want to interrupt this waiting. In this case, we handle such an exception. This was done after the methodThread.stop
was declared Deprecated, i.e. obsolete and undesirable to use. The reason for this was that when the method was called, stop
the thread was simply "killed", which was very unpredictable. We could not know when the stream would be stopped, we could not guarantee the consistency of the data. Imagine that you are writing data to a file and then the stream is destroyed. Therefore, we decided that it would be more logical not to kill the thread, but to inform it that it should be interrupted. How to respond to this is up to the thread itself. More details can be found in Oracle's " Why is Thread.stop deprecated? ". 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, but print 'Interrupted' right away. This is because we called the interrupt
. This method exposes "internal flag called interrupt status". That is, each thread has an internal flag that is not directly accessible. But we have native methods to interact with this flag. But this is not the only way. A thread can be in the process of executing, not waiting for something, but simply performing actions. But it can foresee that they will want to complete it at a certain moment of its work. 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, you can see that the loop while
will continue until the thread is interrupted from outside. What is important to know about the isInterrupted flag is that if we catch InterruptedException
, the flag isInterrupted
is reset, and then isInterrupted
it will return false. There is also a static method in the Thread class that applies only to the current thread - Thread.interrupted() , but this method resets the flag to false! More details can be found in the chapter " Thread Interruption ".
Join - Waiting for another thread to complete
The simplest type of wait is waiting for another thread to complete.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 for 5 seconds. At the same time, the main thread main will wait until the sleeping thread wakes up and finishes its work. If you look through JVisualVM, then the state of the thread will look like this: Thanks to the monitoring tools, you can see what is going on with the thread. The method join
is quite simple because it is just a method with java code that executes wait
while the thread it is called on lives. As soon as the thread dies (on termination), the wait is interrupted. That's the whole magic of the method join
. Therefore, let's move on to the most interesting.
Concept Monitor
In multithreading, there is such a thing as Monitor. In general, the word monitor from Latin is translated as "overseer" or "supervisor". In the framework of this article, we will try to remember the essence, and for those who want, I ask you to dive into the material from the links for details. Let's start our journey with the Java language specification, that is, with the JLS: " 17.1. Synchronization ". It says the following: It turns out that for the purpose of synchronization between threads, Java uses a mechanism called "Monitor". Each object has a monitor associated with it, and threads can "lock" it or unlock it "unlock". Next, we will find a training tutorial on the Oracle website: " Intrinsic Locks and Synchronization". This tutorial says that synchronization in Java is built around an internal entity, known as an intrinsic lock or monitor lock. Often such a lock is simply called a "monitor". We also again see that every object in Java has an associated intrinsic lock. You can read " Java - Intrinsic Locks and Synchronization ". Next, it is important to understand how an object in Java can be associated with a monitor. Every object in Java has a header (header) - a kind of internal metadata that is not accessible to the programmer from code, but which the virtual machine needs to work with objects correctly.The header of the object includes MarkWord, which looks like this: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");
}
}
}
So, with the help of the keyword, synchronized
the current thread (in which these lines of code are executing) tries to use the monitor associated with the objectobject
and "get lock" or "capture monitor" (the second option is even preferable). If there is no contention for the monitor (i.e. no one else wants to synchronized on the same object), Java may try to perform an optimization called "biased locking". In the title of the object in Mark Word, the corresponding tag and a record about which thread the monitor is attached to will be set. This reduces the overhead of grabbing the monitor. If the monitor has already been bound to another thread before, then such a lock is not sufficient. The JVM switches to the next type of lock - basic locking. It uses compare-and-swap (CAS) operations. At the same time, not Mark Word itself is already stored in the header in Mark Word, but a link to its storage + the tag is changed so that the JVM understands that we are using a basic lock. If there is a rivalry (contention) for the monitor of several threads (one has captured the monitor, and the second is waiting for the monitor to be released), then the tag in Mark Word changes, and Mark Word begins to store a reference to the monitor as an object - some internal JVM entity. As stated in the JEP, in this case, space is required in the Native Heap memory area to store this entity. The reference to the storage location of this internal entity will be in the Mark Word object. Thus, as we can see, the monitor is really a mechanism for ensuring synchronization of access of several threads to shared resources. There are several implementations of this mechanism that the JVM switches between. Therefore, for simplicity, when talking about a monitor, we are actually talking about locks. and the second is waiting for the monitor to be released), then the tag in Mark Word changes, and Mark Word starts storing a reference to the monitor as an object - some internal JVM entity. As stated in the JEP, in this case, space is required in the Native Heap memory area to store this entity. The reference to the storage location of this internal entity will be in the Mark Word object. Thus, as we can see, the monitor is really a mechanism for ensuring synchronization of access of several threads to shared resources. There are several implementations of this mechanism that the JVM switches between. Therefore, for simplicity, when talking about a monitor, we are actually talking about locks. and the second is waiting for the monitor to be released), then the tag in Mark Word changes, and Mark Word starts storing a reference to the monitor as an object - some internal JVM entity. As stated in the JEP, in this case, space is required in the Native Heap memory area to store this entity. The reference to the storage location of this internal entity will be in the Mark Word object. Thus, as we can see, the monitor is really a mechanism for ensuring synchronization of access of several threads to shared resources. There are several implementations of this mechanism that the JVM switches between. Therefore, for simplicity, when talking about a monitor, we are actually talking about locks. in this case, space is required in the Native Heap memory area to store this entity. The reference to the storage location of this internal entity will be in the Mark Word object. Thus, as we can see, the monitor is really a mechanism for ensuring synchronization of access of several threads to shared resources. There are several implementations of this mechanism that the JVM switches between. Therefore, for simplicity, when talking about a monitor, we are actually talking about locks. in this case, space is required in the Native Heap memory area to store this entity. The reference to the storage location of this internal entity will be in the Mark Word object. Thus, as we can see, the monitor is really a mechanism for ensuring synchronization of access of several threads to shared resources. There are several implementations of this mechanism that the JVM switches between. Therefore, for simplicity, when talking about a monitor, we are actually talking about locks. between which the JVM switches. Therefore, for simplicity, when talking about a monitor, we are actually talking about locks. between which the JVM switches. Therefore, for simplicity, when talking about a monitor, we are actually talking about locks.
Synchronized and waiting by lock
With the concept of a monitor, as we saw earlier, the concept of a "synchronization block" (or, as it is also called, a critical section) is closely related. Let's 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 sends the task to a new thread, and then immediately "captures" the lock and performs a long operation (8 seconds) with it. All this time, the task cannot enter the block for its execution synchronized
, because location is already taken. If the thread cannot get a lock, it will wait for it at the monitor. As soon as it receives, it will continue execution. When a thread exits the monitor, it releases the lock. In JVisualVM it would look like this: As you can see, the status in JVisualVM is called "Monitor" because the thread is blocked and cannot take the monitor. In the code, you can also find out the state of the thread, but the name of this state does not match the terms of JVisualVM, although they are similar. In this case, th1.getState()
the loop for
will return BLOCKED, because while the loop is running, the monitor lock
is occupied main
by the thread, and the thread th1
is blocked and cannot continue until the lock is returned. In addition to synchronization blocks, an entire method can be synchronized. For example, a method from a class HashTable
:
public synchronized int size() {
return count;
}
In one unit of time, this method will be executed by only one thread. But do we need a lok? Yes I need it. In the case of object methods, the lock will be this
. There is an interesting discussion on this topic: " Is there an advantage to use a Synchronized Method instead of a Synchronized Block? ". If the method is static, then the lock will not be this
(because for a static method it cannot be this
), but a class object (For example, Integer.class
).
Wait and waiting on the monitor. notify and notifyAll methods
Thread has another wait method, which is also associated with the monitor. Unlikesleep
and join
, it cannot be called just like that. And his name is wait
. The method is executed wait
on the object on whose monitor we want to wait. Let's see an example:
public static void main(String []args) throws InterruptedException {
Object lock = new Object();
// task будет ждать, пока его не оповестят через lock
Runnable task = () -> {
synchronized(lock) {
try {
lock.wait();
} catch(InterruptedException e) {
System.out.println("interrupted");
}
}
// После оповещения нас мы будем ждать, пока сможем взять лок
System.out.println("thread");
};
Thread taskThread = new Thread(task);
taskThread.start();
// Ждём и после этого забираем себе лок, оповещаем и отдаём лок
Thread.currentThread().sleep(3000);
System.out.println("main");
synchronized(lock) {
lock.notify();
}
}
In JVisualVM it would look like this: To understand how this works, remember that the wait
and methods notify
refer to java.lang.Object
. It seems odd that the thread-related methods are in the Object
. But here lies the answer. As we remember, every object in Java has a header. The header contains various service information, including information about the monitor - data on the blocking state. And as we remember, each object (i.e. each instance) has an association with an internal JVM entity called a lock (intrinsic lock), which is also called a monitor. In the example above, in the task task, it is described that we enter the synchronization block on the monitor associated with lock
. If it is possible to get a lock on this monitor, thenwait
. The thread executing this task will release the monitor lock
, but queue up threads waiting for notification on the monitor lock
. This queue of threads is called WAIT-SET, which more correctly reflects the essence. It's more of a set, not a queue. The thread main
creates a new thread with the task task, starts it and waits 3 seconds. This allows, with a high degree of probability, a new thread to grab the lock before the thread main
and queue up on the monitor. After that, the thread main
itself enters the synchronization block by itself lock
and performs notification of the thread on the monitor. After the notification is sent, the thread main
releases the monitor lock
, and the new thread (that was previously waiting) waits for the monitor to be releasedlock
, continues execution. It is possible to send a notification to only one of the threads ( notify
) or to all threads from the queue at once ( notifyAll
). You can read more in " Difference between notify() and notifyAll() in Java ". It is important to note that the order of notification depends on the JVM implementation. You can read more in " How to solve starvation with notify and notifyall? ". Synchronization can be performed without specifying an object. This can be done when not a separate piece of code is synchronized, but the whole method. For example, for static methods, the lock will be the 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");
}
}
From the point of view of using locks, both methods are the same. If the method is not static, then synchronization will be performed according to the current instance
, that is, according to this
. By the way, earlier we said that using the method getState
you can get the status of the stream. So, the thread that queues up on the monitor, the status will be WAITING or TIMED_WAITING, if the method wait
has a timeout limit specified.
Thread life cycle
As we have seen, the flow changes its status in the process of life. In fact, these changes are the life cycle of the thread. When a thread is first created, it has the NEW status. In this position, it is not yet running and the Java Thread Scheduler does not yet know anything about the new thread. In order for the thread scheduler to know about a thread, you must call thethread.start()
. The thread will then enter the RUNNABLE state. There are many incorrect schemes on the Internet where the Runnable and Running states are separated. But this is a mistake, because Java does not distinguish between "ready to run" and "running (running)" statuses. When a thread is alive but not active (not Runnable), it is in one of two states:
- BLOCKED - waits for entry into the protected (protected) section, i.e. into
synchonized
a block. - WAITING - waits for another thread by condition. If the condition is true, the thread scheduler starts the thread.
getState
. Streams also have a method isAlive
that returns true if the stream is not Terminated.
LockSupport and Thread Parking
Since Java 1.6 there is an interesting mechanism called LockSupport . This class associates with each thread that uses it a "permit" or permission. The method callpark
returns immediately if a permit is available, occupying that same permit during the call. Otherwise, it is blocked. The method call unpark
makes the permit available if it is not already available. Permit is only 1. In the Java API, for LockSupport
refer to a certain Semaphore
. 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) {
// Просим разрешение и ждём, пока не получим его
e.printStackTrace();
}
System.out.println("Hello, World!");
}
}
This code will wait forever, because the semaphore is now 0 permit. And when it is called in the code acquire
(i.e. request permission), then the thread waits until the permission is received. Since we are waiting, we must process InterruptedException
. Interestingly, a semaphore implements a separate thread state. If we look in JVisualVM, we will see that our state is not Wait, but Park. Let's look at another example:
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
//Запаркуем текущий поток
System.err.println("Will be Parked");
LockSupport.park();
// Как только нас распаркуют - начнём действовать
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 status will be WAITING, but JVisualVM distinguishes wait
from synchronized
and park
from LockSupport
. Why is this one so important LockSupport
? Let's go back to the Java API and look at Thread State WAITING . As you can see, there are only three ways to get into it. 2 ways are wait
and join
. And the third one is LockSupport
. Loks in Java are built in the same way LockSupport
and represent higher-level tools. Let's try to use it. Let's look at, for example 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();
}
}
As in previous examples, everything is simple here. lock
waits for someone to release the resource. If we look in JVisualVM, we will see that the new thread will be parked until main
the thread gives it a lock. You can read more about locks here: " Multi-threaded programming in Java 8. Part two. Synchronizing access to mutable objects " and " Java Lock API. Theory and usage example ". To better understand the implementation of locks, it is helpful to read about Phaser in the overview " The Phaser Class ". And speaking of various synchronizers, the article on Habré " Java.util.concurrent.* Synchronizer Reference " is required reading .
Total
In this overview, we looked at the main ways threads interact in Java. Additional material:- Monitors – The Basic Idea of Java Synchronization
- java.util.concurrent.* synchronizers reference
- Answering multithreading interview questions
GO TO FULL VERSION