We know that the concurrent operations implemented by java must be finally completed by our CPU. In the meantime, we compiled the java source code into a .class file, then loaded, and then executed by the virtual machine execution engine, interpreted as assembly language, then converted to operating system instructions, then converted to 1, 0, and finally the CPU is recognized and executed.
When we mention the concurrency of Java, we can't help but think of common keywords in Java: volatile and synchronized. Next, we will analyze them from these two shutdown words:
The underlying implementation principle of volatile
Implementation Principles and Applications of Synchronized
volatile
Speaking of volatile, the interviewer is the favorite question to ask in Java interviews. When we see it, the first thing we think of is to maintain visibility between threads. It is a lightweight synchronized, which in some cases can replace synchronized.
The role of volatile:
For a variable modified by volatile, the Java memory model will ensure that the variable values seen by all threads are consistent.
How volatile works:
We can define a volatile variable, assign it values, and use tools to obtain the assembly instructions generated by the jit compiler. We will find that when writing to the volatile variable, there will be an additional instruction: an instruction prefixed with lock:
The lock prefix instruction causes two things back to the multi-core processor:
① Write back the data of the current processor cache line to memory.
②This write-back memory operation will invalidate the data cached memory address in other CPUs.
When we know the above two points, it is not difficult for us to understand the mechanism of the volatie variable.
Under multiple processors, in order to ensure that the cache of each processor is consistent, a cache consistency protocol will be implemented. Each processor sniffs the propagated data on the bus to check whether the cached value has expired.
synchronized
When thinking of multi-thread concurrency, the first thing I think of is synchronized. Translated as synchronization. We all know that it is a heavyweight lock. When using it for a method or code block, when a thread obtains this lock, other threads will fall into a suspended state, which will appear in sleep state in Java. We all know that the thread's suspension and running time must be transferred to the kernel state of the operating system (the corresponding to the kernel state is the user state), which is particularly wasteful of CPU resources, so this heavyweight lock is veritable!
However, after java SE 1.6, the Java maintenance team has performed a series of optimizations on it (these optimizations are discussed one by one), so it is not that "heavy", and the reentrant lock, which had advantages in the past, has become less advantageous (ReentrantLock).
Let’s talk about synchronized in the following aspects:
The basics of synchronized to achieve synchronization
How synchronized implements lock
Positive lock, lightweight lock (spin lock), heavyweight lock
Lock upgrade
How to implement atomic operations in Java
①The basics of synchronized to achieve synchronization:
We can see synchronized in development or in Java source code, such as HashTable, StringBuilder and other places. There are two common ways:
Ⅰ, Synchronization method
The synchronization method only needs to be synchronized before the method. When one thread executes it, other threads will fall into waiting until it releases the lock. The use of methods can be divided into two types: for ordinary synchronization methods and for static methods. The difference between them is that the locked objects are different. The locked position of ordinary methods is the current object, and the locked position of static methods is the Class object of the current class.
Ⅱ, Synchronization Method Block
The synchronization method block locks the object configured in the brackets after Synchronized. This object can be a value and any variable or object.
②How synchronized implements lock:
In the jvm specification, you can see the implementation principle of synchronized in jvm. jvm implements synchronization of synchronization methods and code blocks based on entering and exiting the Monitor object. The code block is implemented using monitorenter and monitorexit instructions. The synchronization method is not specifically given in the jvm specification. However, I believe that the specific principles should be different. It is nothing more than compiling the java source code into a class file, and marking the synchronized method in the class bytecode file. This method will be synchronized when the bytecode engine executes this method.
③Deflection lock, lightweight lock (spin lock), heavyweight lock:
Before talking about locks, we need to know the java object header and the java object header:
The lock used by synchronized is stored in the java object header. The java object header has 32bit/64bit (depending on the number of bits of the operating system) length MarkWord stores the hashCode and lock information of the object. There are 2bit spaces in MarkWord to represent the state of the lock 00, 01, 10, 11, respectively, representing lightweight locks, bias locks, heavyweight locks, and GC marks.
Positive lock: Positive lock is called an eccentric lock. From the name we can see that it is a lock that tends toward a certain thread.
In actual development, we found that multi-thread concurrency, most of the synchronization methods are performed by the same thread, and the probability of multiple threads competing for one method is relatively low, so repeated acquisition and release of locks will cause a lot of resource waste. Therefore, in order to make the thread obtain a lock at a lower cost, bias lock is introduced. When a thread accesses a synchronization block and acquires a lock, the thread ID of the bias lock will be stored in the lock record in the stack frame of the object header and thread. In the future, when the thread enters and exits the synchronization block, it does not need to perform CAS operations to lock and unlock. It is only necessary to simply check whether there is a bias lock pointing to the current markWord in the object header (in MarkWord, there is a bias lock flag bit to indicate whether the current object supports bias lock. We can use the jvm parameter to set the bias lock).
Regarding the release of biased locks, biased locks use the mechanism of releasing the lock until competition exists, so the thread holding the biased lock will release the lock when other threads try to compete for biased locks.
Note: In java6, 7, bias lock is started by default
Lightweight lock:
A lightweight lock is that before executing the synchronization block, jvm will create a space for storing the lock record in the stack frame of the current thread, and copy the MarkWord in the object header into it. Then the thread will try to replace the MarkWord in the object header with a pointer to the lock record. If it is successful, the current thread obtains the lock. If it fails, it means that other threads compete for the lock, and the current thread will spin to obtain the lock.
④ Lock upgrade:
If the current thread cannot try the above method to obtain the lock, it means that the current lock is in competition and the lock will be upgraded to a heavyweight lock.
The difference between lightweight lock and biased lock:
Lightweight locks use CAS operations to eliminate mutexes used in synchronization without competition, while biased locks remove the entire synchronization without competition without competition, and even CAS operations are not done!
⑤ How to implement atomic operations in java:
Before understanding how Java implements atomic operations, we need to know how processors implement atomic operations:
Processors are generally divided into two ways to perform atomic operations: cache locking and bus locking, among which cache locking is better, while bus locking is more resource-consuming. (We will not explain too much about the two locking methods here, but there will be detailed explanations in the operating system)
Java uses (mostly) loop CAS to implement atomic operations, but using CAS to implement atomic operations will also cause some of the following classic problems:
1) ABA problem
AtomicStampedReference class is provided in jdk to resolve (providing checking expected references and expected flags)
2) Long cycle time and high overhead
Can't solve this, this is a common problem of circulation
3) Only atomic operations of a shared variable can be guaranteed
A AtomicReference is provided in jdk to solve the problem, placing multiple shared variables in a class for CAS operations.