Using the Synchronized keyword to solve the concurrency problem is the simplest way. We only need to use it to modify the code block, method or field properties that need to be processed concurrently. The virtual machine automatically locks and releases the lock for it, and blocks the thread that cannot obtain the lock on the corresponding blocking queue.

The basic use

When we introduced the basic concepts of threads in our previous article, we mentioned the benefits of multithreading, such as maximizing CPU efficiency, more friendly interactions, and so on, but also raised issues with it, such as race conditions and memory visibility issues.

Let’s use an example from the previous article:

One hundred threads randomly add one to count. Since the increment operation is non-atomic, the abnormal access between multiple threads leads to the uncertainty of the final value of count, and the expected result can never be obtained.

Synchronized is an instant solution:

The code has been slightly modified so that no matter how many times you run the program, or if you increase the number of concurrent requests, the final count value is always the correct 100.

What does that mean roughly?

In JAVA, there is a “built-in lock” on each object, whereas synchronized code attempts to acquire the lock on an object before being executed by a thread. If successful, it enters and executes, otherwise it will block on the object.

In addition to modifying code blocks, synchronized can also modify methods directly, for example:

public synchronized void addCount() {... }Copy the code
public static synchronized void addCount() {... }Copy the code

These are two different ways of using synchronized. Synchronized uses instance methods that are modified with synchronized, and synchronized uses the “built-in lock” of the instance to which the current method is called. That is, the addCount method attempts to acquire the lock of the calling instance object before calling it.

The latter addCount method is a static method, so synchronized uses the lock of the class object that addCount belongs to.

Synchronized is a simple way to lock and release the lock. It is encapsulated by the JVM, so let’s take a look at how synchronized implements this indirect locking mechanism.

Basic Implementation Principles

Let’s start with a simple piece of code:

public class TestAxiom {
    private int count;

    @Test
    public void test() throws InterruptedException { synchronized (this){ count++; }}}Copy the code

This is a very simple piece of code that uses synchronized to modify the code block and protect the count++ operation. Now let’s decompile:

As you can see, the compiler adds a monitorenter directive before the count++ directive, and a monitorexit directive at the end of the count++ directive. These are essentially two lock release instructions, but we’ll see more about that later.

In addition, our synchronized method does not have these two instructions after decompilation, but the compiler sets a flag bit, ACC_SYNCHRONIZED, in the flags property of the method table.

Thus, each thread checks to see if the status bit is 1 before calling the method. If the status is 1, this is a synchronous method. It first attempts to acquire the built-in lock of the current instance object by monitorenter, and releases the lock by Monitorexit at the end of the method execution.

The essence is the same, except that synchronized methods are an implicit implementation. Let’s take a look at the details of the built-in lock.

An object in Java consists mainly of three types of data:

  • Object header: Also called Mark Word. It stores the hash value of the object and related lock information.
  • Instance data: The data saved for the current object, including the parent class attribute information.
  • Padding data: This part is used for byte padding if the current object is less than a multiple of 8 bytes because the JVM requires that each object’s starting address be a multiple of 8 bytes.

Our “built-in lock” is inside the object header, and a basic structure of Mark Word looks like this:

Forget about lightweight locking, weight locking, bias locking, spin locking, this is a lock optimization mechanism for virtual machines that uses lock inflation to optimize performance. We’ll talk about this in more detail later, but you can think of it all as a lock.

Each lock will have a flag bit to distinguish the lock type and a pointer to the lock Record, that is, the lock pointer will be associated with another structure, the Monitor Record.

The Owner field stores the unique identification number of the thread that owns the current lock. When a thread owns the lock, it will write its own thread number into this field. If a thread finds that the Owner field is neither null nor its own thread number, it will be blocked on Monitor’s blocking queue until a thread steps out of the synchronized code block and initiates a wake up operation.

To summarize, synchronized code blocks or methods insert two additional instructions in the compiler, and Monitorenter checks for object headers, corresponding to a Monitor structure whose Owner field is already occupied. The current thread is blocked on a blocking queue in Monitor until the thread holding the lock releases the lock and triggers a new wave of lock contention.

Several characteristics of synchronized

1. Reentrancy

An object often has multiple methods, some of which are synchronous and some of which are asynchronous. If a thread has acquired the lock of an object and entered one of its synchronized methods, and the synchronized method also needs to call another synchronized method of the same instance, does the lock need to be contended again?

This is re-contendable for some locks, but synchronized is “reentrant,” meaning that all methods of an object can be called without contendable if the current thread has acquired the lock.

The monitorenter directive finds Monitor, checks that the Owner field is equal to the thread number of the current thread, and increments the Nest field by one, indicating that the current thread holds the lock on the object multiple times. Each call to MonitoreXit subtracts Nest by one.

2. Memory visibility

To quote an example from the previous article:

Thread ThreadTwo keeps listening for flag values, but our main thread changes flag values. Due to memory visibility, ThreadTwo can’t see them, so the program keeps loping.

Synchronized can, in a sense, solve this memory visibility problem, modifying the code as follows:

The main thread acquires obj’s built-in lock, and then starts ThreadTwo, which is blocked because it cannot acquire OBJ’s lock. That is, it knows that another thread is already manipulating the shared variable, so it must re-read the shared variable from memory when it acquires the lock.

Our main thread will flush the values of all global variables in the private working memory to the memory space when the lock is released, thus achieving memory visibility between multiple threads.

Note, of course, that synchronized modified blocks refresh their own global variables when they release the lock, but another thread must also reread them from memory in order to see them. In general, the synchronized thread does not read data from the memory when you add synchronized, but only when it fails to compete for a lock and learns that another thread is modifying the shared variable, it will brush the memory data again after it owns the lock.

You could also try giving a ThreadTwo thread an object instead of competing for the obj lock, and the result would still be an infinite loop, with flag being a cached version of the initial data that ThreadTwo read from memory when it started.

But honestly, synchronization is expensive to solve the problem of memory visibility. It requires locking and releasing locks, and even blocking and waking up threads. We generally use the keyword volatile to modify variables directly, so that reading and modifying variables are directly mapped to memory. Does not pass through thread local private working memory.

Synchronized is a synchronized keyword that has been optimized for use in JDK versions in recent years, including spin locks, bias locks, and lock inflation between heavyweight locks. This optimization is what makes synchronized as good as Lock today.


All the code, images and files in this article are stored in the cloud on my GitHub:

(github.com/SingleYam/o…).

Welcome to follow the wechat official account: OneJavaCoder. All articles will be synchronized on the official account.