JavaRush /Java Blog /Random EN /You Can't Spoil Java with Threads: Part II - Synchronizat...
Viacheslav
Level 3

You Can't Spoil Java with Threads: Part II - Synchronization

Published in the Random EN group

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. You Can't Screw Java with Threads: Part II - Synchronization - 1

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() javadocYou Can't Screw Java with Threads: Part II - Synchronization - 2 " on the topic of method documentation . If you read it, it is clear that in fact the methodyieldyieldjust 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. Save HelloWorldApp.javathe 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.javaand 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Процесса: You Can't Screw Java with Threads: Part II - Synchronization - 3As 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 method Thread.stopwas declared Deprecated, i.e. obsolete and undesirable to use. The reason for this was that when the method was called, stopthe 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 whilewill 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 isInterruptedis reset, and then isInterruptedit 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: You Can't Screw Java with Threads: Part II - Synchronization - 4Thanks to the monitoring tools, you can see what is going on with the thread. The method joinis quite simple because it is just a method with java code that executes waitwhile 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: You Can't Screw Java with Threads: Part II - Synchronization - 5It 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: You Can't Screw Java with Threads: Part II - Synchronization - 6

https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf

An article from Habr is very useful here: " But how does multithreading work anyway? Part I: synchronization ". This article should be supplemented with the description from the Summary of the task block from the JDK bugtaker: " JDK-8183909 ". You can read the same thing in " JEP-8183909 ". So, in Java, a monitor is associated with an object, and a thread turns out to block this thread, or else they say "get a lock". The simplest example:
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, synchronizedthe current thread (in which these lines of code are executing) tries to use the monitor associated with the objectobjectand "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. You Can't Screw Java with Threads: Part II - Synchronization - 7

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: You Can't Screw Java with Threads: Part II - Synchronization - 8As 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 forwill return BLOCKED, because while the loop is running, the monitor lockis occupied mainby the thread, and the thread th1is 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). You Can't Screw Java with Threads: Part II - Synchronization - 9

Wait and waiting on the monitor. notify and notifyAll methods

Thread has another wait method, which is also associated with the monitor. Unlike sleepand join, it cannot be called just like that. And his name is wait. The method is executed waiton 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: You Can't Screw Java with Threads: Part II - Synchronization - 10To understand how this works, remember that the waitand methods notifyrefer 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 maincreates 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 mainand queue up on the monitor. After that, the thread mainitself enters the synchronization block by itself lockand performs notification of the thread on the monitor. After the notification is sent, the thread mainreleases 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 getStateyou 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 waithas a timeout limit specified. You Can't Screw Java with Threads: Part II - Synchronization - 11

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 the thread.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 synchonizeda block.
  • WAITING - waits for another thread by condition. If the condition is true, the thread scheduler starts the thread.
If a thread is waiting on time, it is in the TIMED_WAITING state. If the thread is no longer running (completed successfully or with an exception), it goes into the TERMINATED state. To find out the state of a thread (its state), the method is used getState. Streams also have a method isAlivethat returns true if the stream is not Terminated.

LockSupport and Thread Parking

Since Java 1.6 there is an interesting mechanism called LockSupport . You Can't Screw Java with Threads: Part II - Synchronization - 12This class associates with each thread that uses it a "permit" or permission. The method call parkreturns immediately if a permit is available, occupying that same permit during the call. Otherwise, it is blocked. The method call unparkmakes the permit available if it is not already available. Permit is only 1. In the Java API, for LockSupportrefer 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. You Can't Screw Java with Threads: Part II - Synchronization - 13Let'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 waitfrom synchronizedand parkfrom 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 waitand join. And the third one is LockSupport. Loks in Java are built in the same way LockSupportand 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. lockwaits for someone to release the resource. If we look in JVisualVM, we will see that the new thread will be parked until mainthe 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: #Viacheslav
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION