This is the second day of my participation in the November Gwen Challenge. Check out the details: the last Gwen Challenge 2021

Introduction: Thread safety issues

What are thread safety issues? When multiple threads concurrently access a Java object and the object behaves consistently and correctly no matter how the system schedules the threads, we say that operations on the object are thread-safe. Instead, operations on this thread are not thread-safe, and a thread-safe problem occurs.

This article answers the following questions: How does synchronize ensure thread safety? How does one add a lock? Where is the lock information placed in the Object? How do I view the lock upgrade process? And so on.

The use of Synchronize and the difference between Synchronize and ReentryLock will be described in the next blog about Lock.

The core principles of Java built-in lock

Java built-in locks are mutually exclusive (exclusive) locks. When thread A holds the lock on an object and thread B tries to acquire the built-in lock on the object, thread B will wait or block until thread A completes and then releases the lock (which can also be released by throwing an exception). If thread A does not release the lock, thread B will wait forever.

Every object in Java is already available as a built-in lock. The lock is automatically acquired when a thread enters a synchronized block and released when it exits the block.

1. Synchronized

The synchronize keyword can be used in three different ways:

1. Decorate a common method

public synchronized void add1(a) {
   val ++;
}
Copy the code

At this point, only one thread can enter the synchronization method of the object. The lock is applied to the object, the object’s this property.

2. Decorate the code block

public void add3(a) {
   synchronized (this) { val ++; }}Copy the code

Slightly different from the preceding code, synchronize modifies a single piece of code because in many cases it is not necessary to lock the entire method, but only a portion of it to ensure thread-safe execution.

Another difference is that you can use Synchronize to wrap this object or other objects for a more flexible locking operation.

3. Decorate a static method

public static synchronized void add2(a) {
   val ++;
}
Copy the code

Java objects can be divided into two main categories: Object objects, which are allocated in the JVM heap, and Class objects, which are stored in the method area (called the meta space in the Java1.8 HotSpot implementation). A Class file in the JVM only has one Class Class. There are many objects instantiated by Class. When you modify a static method, you add a lock to a Class object.

Summary: The above three methods of locking are different, but similar in nature, in that they all lock a Java object so that only one thread at a time can access a synchronized code block. Ok, so with that in mind, let’s take a look at his implementation, Synchronize and how does that implementation lock an object?

2. Java object structure and built-in lock

Before introducing built-in locks, it is necessary to introduce the structure of Java objects.

A Java object can be divided into three parts:

1) Object header

The object header contains three fields: the first is Mark Word, which stores the object’s GC information, lock information, hashcode value, and so on. The second is a Class Pointer that holds a Class Pointer to which Class the object is an instance. The third is Array Length, which is an optional field that exists only if the object is an Array and is used to record the Length of the Array.

2) Object body

This section contains the instance variables of the object, contains the attributes of the parent class, and is aligned with 4 bytes.

3) Align bytes

It is used to ensure that the number of bytes of memory used by the object is an integer multiple of 8. Since the object header is 8 bits, it is only necessary to ensure that the object body is also a multiple of 8. If the object instance variable data is not a multiple of 8, it needs to be filled to ensure the alignment of 8 bytes.

Fields such as Mark Word, Class Pointer, Array Length, and so on have lengths that are related to the number of bits in the JVM. The Mark Word length is one Word size for the JVM, meaning that the Mark Word is 32 bits for a 32-bit JVM and 64 bits for a 64-bit JVM. The length of the Class Pointer field is also one Word of the JVM’s Mark Word, i.e., 32 bits for a 32-bit JVM and 64 bits for a 64-bit JVM. So, in a 32-bit JVM, the Mark Word and Class Pointer parts are both 32-bit; In a 64-bit JVM, both parts of Mark Word and ClassPointer are 64-bit.

Structure information of 32-bit Mark Word in different lock states

The 64-bit Mark Word has a similar structure to the 32-bit Mark Word,

Structure information for 64-bit Mark Work in different lock states

Object add built-in lock and lock upgrade process

No lock –> Bias lock:

When a thread enters a block of synchronized code and finds that the object has no thread usage, the object uses CAS (null, threadID), null is the expected value, threadID is the value to be written, and writes its threadID into the object’s Mark Word. That is, there is no threadID in the object. Then the object changes from the non-locked state to the biased lock state, and the lock stays the same as 01. Change the biased lock flag bit from 0 to 1.

Partial lock –> Lightweight lock:

When a thread enters the synchronization code block, use CAS (null, threadID) to write its threadID into the Mark Word of the object. If the writing fails, it indicates that the thread has been occupied at this time, then revoke the biased lock status of the object and upgrade to lightweight lock. The lock bit in the Mark Word object changes from 01 to 00.

Lightweight lock –> Heavyweight lock:

Lightweight locks are intended to reduce the probability of multiple threads entering the underlying operating system Mutex Lock, not to replace the operating system Mutex Lock. Therefore, in a scenario of intense contention, lightweight locks can swell into heavyweight locks based on the operating system kernel mutex implementation. The conditions for upgrading a lightweight lock to a heavyweight lock are more complex, which we will explore in detail in the next section.