User Viacheslav
Viacheslav
Level 3
St. Petersburg

Better together: Java and the Thread class. Part II — Synchronization

Published in the Java Developer group

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

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 - 2There is a bug (JDK-6416721: (spec thread) Fix Thread.yield() javadoc) logged for the 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 named HelloWorldApp.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 - 3As you can see, our thread now has the "Sleeping" status. In fact, there is a more elegant way to help our thread have sweet dreams:

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 an InterruptedException. 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 - 4Thanks to monitoring tools, you can see what is going on with the thread. The 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 - 5It turns out that Java uses a "monitor" mechanism for synchronization between threads. A monitor is associated with each object, and threads can acquire it with 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://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf

Here's a JavaWorld article that is very useful: How the Java virtual machine performs thread synchronization . This article should be combined with the description from the "Summary" section of the following issue in the JDK bug-tracking system: JDK-8183909. You can read the same thing here: JEP-8183909. So, in Java, a monitor is associated with an object and is used to block a thread when the thread tries to acquire (or get) the lock. Here's the simplest example:

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

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 - 8As you can see in JVisualVM, the status is "Monitor", meaning that the thread is blocked and cannot take the monitor. You can also use code to determine a thread's status, but the names of status determined this way do not match the names used in JVisualVM, though they are similar. In this case, the 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). Better together: Java and the Thread class. Part II — Synchronization - 9

Wait (waiting for a monitor). notify() and notifyAll() methods

The Thread class has another waiting method that is associated with a monitor. Unlike sleep() 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 - 10To understand how this works, remember that the 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://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 the thread.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.
If the condition is satisfied, then the thread scheduler starts the thread. If the thread is waiting up to a specified time, then its status is TIMED_WAITING. If the thread is no longer running (it is finished or an exception was thrown), then it enters the TERMINATED status. To find out a thread's state, use the 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 - 12This class associates a "permit" with each thread that uses it. A call to the 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 - 13Let's look at another example:

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 LockSupport 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 Better together: Java and the Thread class. Part III — Interaction Better together: Java and the Thread class. Part IV — Callable, Future, and friends Better together: Java and the Thread class. Part V — Executor, ThreadPool, Fork/Join Better together: Java and the Thread class. Part VI — Fire away!
Comments (1)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
Thomas Hauck Level 0
5 May 2020
The link to "How Multithreading works" has been updated to