JavaRush /Java Blog /Random EN /You can't ruin Java with a thread: Part II - synchronizat...
Viacheslav
Level 3

You can't ruin Java with a thread: Part II - synchronization

Published in the Random EN group

Introduction

So, we know that there are threads in Java, which you can read about in the review “ You Can’t Spoil Java with a Thread: Part I - Threads ”. Threads are needed to do work simultaneously. Therefore, it is very likely that the threads will somehow interact with each other. Let's understand how this happens and what basic controls we have. You can't ruin Java with a thread: Part II - synchronization - 1

Yield

The Thread.yield() method is mysterious and rarely used. There are many variations of its description on the Internet. 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 its status from running to runnable (although there is no division into these statuses, and Java does not distinguish between them). But in reality, everything is much more unknown and, in a sense, simpler. You can't ruin Java with a thread: Part II - synchronization - 2On the topic of method documentation, yieldthere is a bug " JDK-6416721: (spec thread) Fix Thread.yield() javadoc ". If you read it, it is clear that in fact the method yieldonly conveys some recommendation to the Java thread scheduler that this 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 from some other factors. All the confusion was most likely due to the rethinking of multithreading during the development of the Java language. You can read more in the review " Brief Introduction to Java Thread.yield() ".

Sleep - Falling asleep thread

A thread may fall asleep during its execution. 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 executed, 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 Java code, but he can, through the JVM, ask the scheduler to pause the thread for a while, to “put it to sleep.” You can read more in the articles " Thread.sleep() " and " How Multithreading works ". Moreover, you can find out how threads work in Windows OS: " Internals of Windows Thread ". Now we will see it with our own eyes. Let's save the following code to a file HelloWorldApp.java:
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 a task that waits 60 seconds, after which the program ends. We compile javac HelloWorldApp.javaand run java HelloWorldApp. It is better to launch in a separate window. For example, on Windows it would be like this: start java HelloWorldApp. Using the jps command, we find out the PID of the process and open the list of threads using jvisualvm --openpid pidПроцесса: You can't ruin Java with a thread: Part II - synchronization - 3As you can see, our thread has entered 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've probably noticed that we process everywhere InterruptedException? Let's understand why.

Interrupting a thread or Thread.interrupt

The thing is that while the thread is waiting in sleep, someone may want to interrupt this wait. In this case, we handle such an exception. This was done after the method Thread.stopwas declared Deprecated, i.e. outdated and undesirable for 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 flow 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, they decided that it would be more logical not to kill the flow, but to inform it that it should be interrupted. How to react to this is up to the flow 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 will not wait 60 seconds, but will immediately print 'Interrupted'. This is because we called the thread's method interrupt. This method sets "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 this is not the only way. A thread can be in the process of execution, not waiting for something, but simply performing actions. But it can provide that they will want to complete it at a certain point in 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 run until the thread is interrupted externally. The important thing to know about the isInterrupted flag is that if we catch it InterruptedException, the flag isInterruptedis reset, and then isInterruptedit will return false. There is also a static method in the Thread class that only applies to the current thread - Thread.interrupted() , but this method resets the flag to false! You can read more 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 will wait until the sleeping thread wakes up and finishes its work. If you look through JVisualVM, the state of the thread will look like this: You can't ruin Java with a thread: Part II - synchronization - 4Thanks to monitoring tools, you can see what is happening with the thread. The method joinis quite simple, because it is simply a method with java code that executes waitwhile the thread on which it is called is alive. Once the thread dies (at termination), the wait is terminated. That's the whole magic of the method join. Therefore, let's move on to the most interesting part.

Concept Monitor

In multithreading there is such a thing as Monitor. In general, the word monitor is translated from Latin as “overseer” or “overseer.” Within 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 JLS: " 17.1. Synchronization ". It says the following: You can't ruin Java with a thread: Part II - synchronization - 5It turns out that for the purpose of synchronization between threads, Java uses a certain mechanism called “Monitor”. Each object has a monitor associated with it, and threads can lock it or unlock it. Next, we will find a training tutorial on the Oracle website: “ Intrinsic Locks and Synchronization ”. This tutorial explains 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 see again that every object in Java has an intrinsic lock associated with it. 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. Each object in Java has a header - a kind of internal metadata that is not available to the programmer from the code, but which the virtual machine needs to work with objects correctly. The object header includes a MarkWord that looks like this: You can't ruin Java with a thread: 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? Part I: synchronization .” To this article it is worth adding a 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 the thread can block this thread, or they also 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, using the keyword, synchronizedthe current thread (in which these lines of code are executed) tries to use the monitor associated with the object objectand “get a lock” or “capture the monitor” (the second option is even preferable). If there is no contention for the monitor (i.e. no one else wants to synchronize on the same object), Java can try to perform an optimization called "biased locking". The title of the object in Mark Word will contain the corresponding tag and a record of which thread the monitor is attached to. This reduces the overhead when capturing the monitor. If the monitor has already been tied to another thread before, then this locking is not enough. The JVM switches to the next locking type - basic locking. It uses compare-and-swap (CAS) operations. At the same time, the header in Mark Word no longer stores Mark Word itself, but a link to its storage + the tag is changed so that the JVM understands that we are using basic locking. If there is 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 entity of the JVM. As stated in the JEP, in this case, space is required in the Native Heap memory area to store this entity. The link to the storage location of this internal entity will be located in the Mark Word object. Thus, as we see, the monitor is really a mechanism for ensuring synchronization of access of multiple 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. You can't ruin Java with a thread: Part II - synchronization - 7

Synchronized and waiting by lock

The concept of a monitor, as we previously saw, is closely related to the concept of a “synchronization block” (or, as it is also called, a critical section). 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 with it (8 seconds). All this time, the task cannot enter the block for its execution synchronized, because the lock is already occupied. If a thread cannot obtain a lock, it will wait at the monitor for it. As soon as it receives it, it will continue execution. When a thread leaves the monitor, it releases the lock. In JVisualVM it would look like this: You can't ruin Java with a thread: Part II - synchronization - 8As you can see, the status in JVisualVM is called "Monitor" because the thread is blocked and cannot occupy the monitor. You can also find out the state of the thread in the code, but the name of this state does not coincide with the JVisualVM terms, 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 working until the lock is returned. In addition to synchronization blocks, an entire method can be synchronized. For example, a method from the class HashTable:
public synchronized int size() {
	return count;
}
In one unit of time, this method will be executed by only one thread. But we need a lock, right? 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(since for a static method it cannot be this), but the class object (For example, Integer.class).

Wait and waiting on the monitor. The notify and notifyAll methods

Thread has another wait method, which is connected to the monitor. Unlike sleepand join, it cannot just be called. 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 will look like this: You can't ruin Java with a thread: Part II - synchronization - 10To understand how this works, you should remember that methods waitrefer notifyto java.lang.Object. It seems strange that thread related methods are in the Object. But herein 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 about the locking state. And as we remember, each object (i.e. each instance) has an association with an internal JVM entity called an intrinsic lock, which is also called a monitor. In the example above, the task describes that we enter the synchronization block on the monitor associated with lock. If it is possible to obtain a lock on this monitor, then wait. The thread executing this task will release the monitor lock, but will join the queue of 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 than a queue. The thread maincreates a new thread with the task task, starts it and waits for 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 which the thread mainitself enters the synchronization block lockand performs notification of the thread on the monitor. After the notification is sent, the thread mainreleases the monitor lock, and the new thread (which was previously waiting) lockcontinues executing after waiting for the monitor to be released. It is possible to send a notification to only one of the threads ( notify) or to all threads in the queue at once ( notifyAll). You can read more in " Difference between notify() and notifyAll() in Java ". It is important to note that the notification order 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 section of code is synchronized, but an entire 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");
	}
}
In terms 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 a thread. So here is a thread that is queued by the monitor, the status will be WAITING or TIMED_WAITING if the method waitspecified a waiting time limit. You can't ruin Java with a thread: Part II - synchronization - 11

Lifecycle of a thread

As we have seen, the flow changes its status in the course of life. In essence, these changes are the life cycle of the thread. When a thread is just created, it has the NEW status. In this position, it has not yet started 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(). Then the thread will go into the RUNNABLE state. There are many incorrect schemes on the Internet where Runnable and Running states are separated. But this is a mistake, because... Java does not differentiate between "ready to run" and "running" statuses. When a thread is alive but not active (not Runnable), it is in one of two states:
  • BLOCKED - awaits entry into a protected section, i.e. to synchonizedthe block.
  • WAITING - waits for another thread based on a condition. If the condition is true, the thread scheduler starts the thread.
If a thread is waiting by time, it is in the TIMED_WAITING state. If the thread is no longer running (completed successfully or with an exception), it goes into TERMINATED status. To find out the state of a thread (its state), the method is used getState. Threads also have a method isAlivethat returns true if the thread is not Terminated.

LockSupport and thread parking

Since Java 1.6 there was an interesting mechanism called LockSupport . You can't ruin Java with a thread: Part II - synchronization - 12This class associates a "permit" or permission with each thread that uses it. The method call parkreturns immediately if a permit is available, occupying that same permit during the call. Otherwise it is blocked. Calling the method unparkmakes permit available if it is not already available. There is only 1 Permit. In the Java API, LockSupporta 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 now has 0 permit. And when called in code acquire(i.e., request permission), the thread waits until it receives permission. Since we are waiting, we are obliged to process it 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 ruin Java with a thread: 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 waitbetween from synchronizedand parkfrom LockSupport. Why is this one so important LockSupport? Let's turn again 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 - this waitand join. And the third one is LockSupport. Locks in Java are built on the same principles LockSupportand represent higher-level tools. Let's try to use one. Let's look, for example, 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();
    }
}
As in previous examples, everything is simple here. lockwaits for someone to release a resource. If we look in JVisualVM, we will see that the new thread will be parked until mainthe thread gives it the lock. You can read more about locks here: " Multithreaded programming in Java 8. Part two. Synchronizing access to mutable objects " and " Java Lock API. Theory and example of use ." To better understand the implementation of locks, it is useful to read about Phazer in the overview " Phaser Class ". And speaking about various synchronizers, you must read the article on Habré “ Java.util.concurrent.* Synchronizers Reference ”.

Total

In this review, 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