Java code is compiled into Java bytecode, which is loaded by the classloader into the JVM. The JVM executes the bytecode, which ultimately needs to be converted into assembly instructions for execution on the CPU. The concurrency mechanism used in the Java italic style depends on the JVM’s implementation and the CPU’s instructions.

1. The application of volatile

Synchronized and volatile play important roles in multithreaded concurrent programming. Volatile is the lightweight synchronized that guarantees “visibility” of shared variables in multiprocessor development. Visibility means that when one thread modifies a shared variable, another thread can read the changed value. Volatile modifiers, when used properly, are cheaper to use and execute than synchronized because they do not cause thread context switching and scheduling.

1.1 Definition and implementation of volatile

Definition: The Java programming language allows threads to access a shared variable. To ensure that a shared variable is updated accurately and consistently, threads should ensure that the variable is acquired separately through an exclusive lock.

The Java language provides volatile, which in some cases is more convenient than locking. If a field is declared volatile, the Java thread memory model ensures that all threads see the variable’s value as consistent.

Two implementation principles for volatile

  1. The Lock prefix instruction causes the processor cache to be written back to memory. The Lock prefix instruction causes the processor’s signal to be declared during the execution of the instruction. In a multiprocessor environment, the LOCK# signal ensures that the processor can monopolize any shared memory during the processor modification process.

  2. Writing one processor’s cache back to memory invalidates another processor’s cache.

2. Implementation principle and application of synchronized

Synchronized has long been an elder figure in multithreaded concurrent programming, with many calling it the heavyweight lock, but with Java SE 1.6 tweaks to synchronized, in some cases it’s not that heavy.

Synchronized is the basis for implementing synchronization: every object in Java can be used as a lock. In the following three forms: 1. For ordinary synchronization methods, the lock is the current instance object. 2. For statically Synchronized methods, the lock is the Class object of the current Class. 3. For Synchronized code blocks, the lock is the object configured in Synchronized parenthesesCopy the code

When a thread view accesses a block of synchronized code, it must first acquire the lock and release the lock when it exits or throws an exception. So where does the lock actually exist? What information is stored inside the lock?

From the JVM specification, you can see how Synchronized is implemented in the JVM. The JVM implements method synchronization and code block synchronization based on entering and exiting Monitor objects, but the implementation details are different. Code block synchronization is implemented using the Monitorenter and Monitorexit directives, while method synchronization is implemented in a different way, the details of which are not specified in the JVM specification. The synchronization of methods is also achieved by these two instructions.

The MonitoRenter directive inserts at the start of the synchronized code block after compilation, while the Monitorexit inserts at the end of the method and at the exception. The JVM ensures that each Monitorenter must have a Monitorexit paired with it. A personality object has a Monitor associated with it. When a Monitor is held, it is locked. When a thread executes a Monitorenter instruction, it attempts to acquire ownership of the object’s monitor, that is, the lock on the object.

2.1 Java Object Headers

Synchronized uses locks that are stored in Java object headers. If the object is an array type, the virtual machine stores the object header with 3 Word widths (Word widths), and if the object is not an array type, it stores the object header with 2 Word widths. In a 32-bit VM, a character width equals 4 bytes, or 32 bits, and in a 64-bit VM, it equals 64 bits.

The length of the Java object header
The length of the content instructions
32/62 bit Mark Word Store objects such as hashCode or lock information
32/62 bit Class Metadata Address A pointer to data stored in an object type
32/62 bit Array Length The length of the array if the current object is an array

The mark Word in the Java object header stores the object’s hashCode, generational age, and lock marker bits by default. The default storage structure for 32-bit JVM Mark Word is as follows:

The storage structure of Java object headers
The lock state 25 bit 4 bit 1 bit Whether bias lock 2 bit Lock flag bit
Unlocked state The object’s hashCode Age of object generation 0 01
During runtime, the data stored in Mark Word changes as the lock flag bit changes. Mark Word may change to store four types of data:
The Mark Word status changes
The lock state 25 bit 4 bit 1 bit 2 bit
23 bit 2 bit Bias lock Lock flag bit
Lightweight lock Pointer to the lock record in the stack 00
Heavyweight lock A pointer to a mutex (heavyweight lock) 10
The GC tag empty 11
Biased locking Thread ID Epoch Age of object generation 1 01
On a 64-bit vm, Mark Wrod is 64-bit and its storage structure is as follows:
The lock state 25 bit 31 bit 1 bit 4 bit 1 bit 2 bit
cms_free Generational age Biased locking Lock flag bit
unlocked unused hashCode 0 01
Biased locking ThreadID(54 bit) Epoch(2 bit) 1 01

3 Lock upgrade and comparison

Java SE 1.6 introduces “biased locks” and “lightweight locks” to reduce performance costs associated with acquiring and releasing locks. In Java SE 1.6, there are four lock states, from lowest to highest: Lockless, biased, lightweight, and heavyweight lock states will gradually escalate with competition. Locks can be upgraded but cannot be downgraded, meaning biased locks cannot be downgraded to biased locks after being upgraded to lightweight locks. The purpose of this policy is to improve the efficiency of acquiring and releasing locks.

3.1 biased locking

The author of HotSpot found through research that in most cases, locks not only do not exist multi-thread contention, but are always acquired by the same thread multiple times. Biased locks are introduced in order to make the cost of lock acquisition lower for the thread. When a thread, when accessing a synchronized block and obtain the lock will lock in the head and stack frame object record store to lock in the thread ID, after this thread synchronization block on the entry and exit for CAS operation is not needed to lock and unlock, only need a simple test whether object Mark of the head Word in store of biased locking points to the current thread. If the test succeeds, the thread has acquired the lock. If the test fails, it is necessary to test whether the Mark of biased lock in Mark Word is set to 1 (indicating that it is currently a biased lock). If not, CAS is used to compete for the lock. If set, try using CAS to point the bias lock of the object header to the current thread.

1. Partial lock revocation

Biased locks use a mechanism that waits until a race occurs to release the lock, so the thread holding the biased lock will release the lock only when other threads attempt to contest the biased lock. Partial lock revocation requires waiting for the global safe point (at which no code is executing). It first suspends the thread with the bias lock, then checks whether the thread with the bias lock is alive, and sets the object header to lock free if the thread is not active. If the thread is still alive, the stack with the biased lock is executed, the lock record of the biased object is traversed, the lock record in the stack and the Mark Word of the object header are either rebiased to other threads, or the lockless Mark object is not suitable for the biased lock, and finally the suspended thread is woken up.

The following figure illustrates the process of acquiring and revoking biased locksCopy the code

2. Close bias lock

Biased locking is enabled by default in the Java 6 and Java 7, but he was in the application startup after a few seconds to activate, deliberately use the JVM parameters if necessary to shut down latency: – XX: BiasedLockingStartupDelay = 0. If you are sure that all locks in your application are normally contested, you can turn off biased locking with the JVM argument: -xx: -usebiasedLocking =false, and your application will enter the lightweight locking state by default.

3.2 Lightweight Lock

1. Lightweight lock plus lock

The JVM will create a space for storing the lock record in the stack frame of the current thread before executing the synchronized code block, and copy the Mark Word in the object header into the lock record, which is officially called the product Mark Word. The thread then tries to use CAS to replace the Mark Word in the object header with a pointer to the lock record. If it succeeds, the current thread acquires the lock; if it fails, other threads compete for the lock and the current thread attempts to acquire the lock using spin.

2. Lightweight lock unlock

The product of the lightweight lock will be replaced by atomic CAS operation when unlocking the product of the herbivore product. If the product is successful, it indicates that there is no competition; if the product fails, it indicates that the current lock is competing, and the lock will expand into a heavyweight lock.

The following is a flow chart of two threads competing for a lock at the same timeCopy the code

Because spin consumes CPU, to avoid useless spin (such as blocking the thread that acquired the lock), once a lock is upgraded to a heavyweight lock, it does not revert to a lightweight lock. When the lock is in this state, other threads trying to acquire the lock will be blocked. When the thread holding the lock releases the lock, these threads will be awakened, and the awakened thread will start a new round of contention for the lock.

3.3 Comparison of advantages and disadvantages of lock

The lock advantages disadvantages Applicable scenario
Biased locking Locking and unlocking require no additional cost, with a nanosecond difference compared to implementing asynchronous methods If there is lock contention between threads, there is additional lock cancellation cost This applies to scenarios where only one thread accesses a synchronized block
Lightweight lock Competing threads do not block, improving the response time of the program If you never get a thread competing for the lock, using spin consumes CPU Pursuit of response time, synchronous block execution is very fast
Heavyweight lock Thread contention does not use spin and does not consume CPU Threads are blocked and response time is slow The pursuit of throughput, synchronous block execution speed is long