On my blog and public account, I have written a number of articles about concurrent programming, and in the previous article we covered two of the most important keywords in concurrent programming in Java: synchronized and volatile

Here’s a quick review:

1. To solve the atomicity, visibility, and order problems in concurrent programming, the Java language provides a series of keywords related to concurrent processing, such as synchronized, volatile, final, concurren package, etc.

2. Synchronized, through locking, can be used as one of the solutions when atomicity, visibility and order are needed, and seems to be “all-purpose”. Indeed, much of the concurrency control can be done using synchronized.

3. Volatile ensures the visibility and ordering of volatile variables in concurrent scenarios by inserting memory barriers before and after volatile operations.

4. Volatile keywords do not guarantee atomicity, while synchronized ensures that synchronized code can only be accessed by one thread at a time through monitorenter and Monitorexit. Can ensure that there will be no CPU time slice switching between multiple threads, can ensure atomicity.

Well, we know that synchronized and volatile are two of the most commonly used keywords in Concurrent programming in Java, and from the previous review, we know that synchronized guarantees no atomicity, visibility, and ordering problems in concurrent programming. Volatile guarantees only visibility and order, so what is volatile if synchronized?

Next, this article discusses why Java should provide the volatile keyword when it already has the synchronized keyword.

The problem of synchronized

Synchronized we all know that synchronized is a mechanism for locking, but it has several drawbacks:

1, performance loss

Synchronized is still a lock, despite a lot of improvements in JDK 1.6 such as adaptive spin, lock elimination, lock coarser, lightweight, and biased locking.

The above optimizations try their best to avoid locking Monitor. However, not all cases can be optimized, and even after optimization, the optimization process is time-consuming.

Therefore, whether using synchronization method or synchronization code block, before the synchronization operation still need to be locked, after the synchronization operation need to be unlocked, this locking, unlocking process is to have performance loss.

In terms of performance comparisons, it is difficult to quantify the performance difference between the two due to the many lock eliminations and optimizations implemented by virtual machines, but one basic principle we can confirm is: Read operations on volatile variables performed almost as well as those on normal variables, but writes were slower because they required memory barriers to be inserted. Even so, volatile was less expensive than locks in most scenarios.

2. Block

In our in-depth understanding of the implementation principle of Synchronized, we introduced the implementation principle of Synchronize. Both ACC_SYNCHRONIZED and Monitorenter and Monitorexit are implemented based on Monitor.

Based on The Monitor object, when multiple threads simultaneously access a piece of synchronized code, they first enter The Entry Set. When one thread obtains The lock of The object, The Owner area can be started. Other threads continue to wait in The Entry Set. When a thread calls the wait method, it releases the lock and enters the wait Set.



Therefore, the Synchronize implementation of a lock is essentially a blocking lock, meaning that multiple threads queue up to access the same shared object.

Volatile is a lightweight synchronization mechanism provided by the Java VIRTUAL machine based on memory barriers. After all, he’s not a lock, so he doesn’t suffer from the blocking and performance loss that synchronized brings.

Additional features of volatile

In addition to the fact that volatile is better than synchronized, volatile has the added benefit of preventing instruction reordering.

Let’s start with an example of what can go wrong with using synchronized instead of volatile, taking the more familiar singleton pattern.

We implement a singleton using a double check lock, without the volatile keyword:

 1   public class Singleton {  
 2      private static Singleton singleton;  
 3       private Singleton (){}  
 4       public static Singleton getSingleton() {5if (singleton == null) {  
 6           synchronized (Singleton.class) {  
 7               if(singleton == null) { 8 singleton = new Singleton(); 9} 10} 11} 12returnsingleton; 14 13}}Copy the code

Singleton = new Singleton() Singleton = new Singleton() Singleton = new Singleton() Singleton = new Singleton() So that’s implementing a singleton.

However, null-pointer exceptions can occur when we use the above singleton in our code. This is a weird situation.

Let’s assume that Thread1 and Thread2 both request Singleton. GetSingleton:

  • Step1,Thread1 executes to line 8 to initialize the object.
  • Step2,Thread2 execute to line 5, check singleton == null.
  • Step3,Thread2 finds singleton! = null, so line 12 returns the singleton.
  • Step4,Thread2 takes the singleton object and performs subsequent operations, such as calling singleton.call().

The above procedure does not appear to be a problem, but in fact it is possible for Thread2 to throw a null-pointer exception when it calls singleton.call() in Step4.

The NPE will be thrown because the singleton object Thread2 gets in Step3 is not a complete object.

What is an incomplete object? What is an incomplete object?

Singleton = new singleton (); Here’s what this line of code does:

  • 1. The virtual machine goes to the new directive and to the constant pool to locate the symbolic reference to the class.
  • Check whether the class represented by the symbol reference has been loaded, parsed, and initialized.
  • 3. The VM allocates memory for objects.
  • 4. The VM initializes all allocated memory space to zero.
  • 5. The VM performs necessary Settings on the object.
  • 6, execute method, member variable initialization.
  • 7. Point a reference to the object to the memory region.

Let’s simplify the process to three steps:

  • A. The JVM allocates a block of memory M for objects
  • B. Initialize the object in memory M
  • C, copy the address of memory M to the singleton variable

The diagram below:



Since assigning the address of memory to the Singleton variable is the last step, Thread2 will block until Thread1 completes this step by checking that singleton==null is true.

However, the problem is that the above procedure is not an atomic operation, and the compiler may reorder the steps if they are rearranged into:

  • A. The JVM allocates a block of memory M for objects
  • C, copy the address of memory to the singleton variable
  • B. Initialize the object in memory M

    The diagram below:



    If singleton==null is used to initialize the object, then the value of singleton==null is used to initialize the object. If singleton==null is used to initialize the object, the value of singleton==null is used to initialize the object. An incomplete Sigleton object is returned because it has not completed initialization.

Once this happens, we get an incomplete Singleton object, and it is highly likely that an NPE exception will occur when we try to use this object.

So, how to solve this problem? Since reordering is causing this problem, it’s ok to avoid reordering.

Therefore, volatile is useful because it avoids instruction reordering. This can be solved by simply changing the code to the following:

 1   public class Singleton {  
 2      private volatile static Singleton singleton;  
 3       private Singleton (){}  
 4       public static Singleton getSingleton() {5if (singleton == null) {  
 6           synchronized (Singleton.class) {  
 7               if(singleton == null) { 8 singleton = new Singleton(); 9} 10} 11} 12returnsingleton; 14 13}}Copy the code

Use volatile constraints on the Singleton to ensure that its initialization is not reordered by instructions. This ensures that Thread2 either doesn’t get the object or gets a complete object.

What about the order assurance of synchronized?

See here may have friends will ask, in the final analysis, the above problem is the occurrence of command rearrangement, in fact, it is a problem of order, not to say synchronized can guarantee order, why not here?

First of all, it’s clear that synchronized does not prohibit instruction rearrangement and processor optimization. So how does he guarantee order?

So this is going to take the idea of order a little bit further. The natural orderliness of Java programs can be summed up in the following sentence: all operations are naturally ordered if viewed within this thread. If you observe another thread in one thread, all operations are unordered.

This sentence is also the original sentence of “Understanding the Java Virtual Machine”, but how to understand? Zhou did not elaborate. I’m going to extend this a little bit, but it’s actually related to the as-if-serial semantics.

The result of a single-threaded program cannot be changed no matter how reordered it is. Compilers and processors, no matter how optimized, must adhere to the as-IF-Serial semantics.

The as-if-serial semantics guarantee that in a single thread, no matter how the instructions are rearranged, the final execution result cannot be changed.

So, going back to the double check lock example, if you look at Thread1 from a single-thread point of view, this optimization doesn’t have any problem and doesn’t have any effect on the execution results of that thread because the compiler follows the as-if-serial semantics.

However, an instruction rearrangement inside Thread1 has an effect on Thread2.

So, we can say that the order guaranteed by synchronized is the order between multiple threads, that is, the locked content is executed by multiple threads in sequence. However, the internal synchronized code is still reordered, but since both the compiler and processor follow the as-if-serial semantics, we can assume that these reorders are negligible within a single thread.

conclusion

This article discusses the importance and irreplaceability of volatile in two ways:

One reason is that synchronized is a locking mechanism and has blocking and performance issues, whereas volatile is not a lock and therefore does not have blocking and performance issues.

On the other hand, because volatile uses memory barriers to help with visibility and order issues, and memory barriers also provide an attachment that disables instruction reordering, there are scenarios where instruction reordering problems can be avoided.

Therefore, in future concurrency control situations where atomicity is not an issue, the volatile keyword is preferred.

The author l Hollis

Source l Hollis



Welcome to follow my wechat public account “Code farming breakthrough”, share Python, Java, big data, machine learning, artificial intelligence and other technologies, pay attention to code farming technology improvement, career breakthrough, thinking transition, 200,000 + code farming growth charge first stop, accompany you have a dream to grow together