preface
Java concurrent programming series second Synchronized, article style is still illustrated, easy to understand, this article takes readers from simple to deep understanding of Synchronized, let readers also can and interviewers crazy line.
Synchronized has long been an elder statesman in concurrent programming. Prior to Jdk 1.6, it was referred to as a heavyweight Lock, and it was cumbersome compared to the locks provided by the J, U, and C packages. Synchronized performance is already very fast.
Outline of the content
Synchronized usage
Synchronized is a synchronization keyword provided by Java. In multi-threaded scenarios, read and write operations on code segments of shared resources (the write operation must be included. Optical reading does not have thread safety problems, because the read operation naturally has thread safety characteristics), and thread safety problems may occur. Synchronized can be used to lock out shared resource sections of code to achieve mutualexclusion and ensure thread safety.
Shared resource code segments are also called critical sections to ensure that critical sections are mutually exclusive. This means that only one thread can execute a critical section, and other threads block and wait to queue.
Synchronized can be eaten in three ways
- Modify normal functions, monitor locks (
monitor
) is the object instance (this
) - Modify static static functions, viewlock (
monitor
Is of the objectClass
Instance (only one per objectClass
Instance) - Modify code block, monitor lock (
monitor
) is the specified object instance
Common function
Ordinary functions use Synchronized simply by placing Synchronized between the access modifier and the function return type.
In multi-threaded scenarios, thread and threadTwo execute the INCR function, and the INCR function is read and written by multiple threads as a code segment of shared resource, which is called the critical region. In order to ensure the critical region is mutually exclusive, Synchronized can be used to modify the INCR function.
public class SyncTest {
private int j = 0;
/** * increment method */
public synchronized void incr(a){
// Critical section code --start
for (int i = 0; i < 10000; i++) {
j++;
}
// Critical section code --end
}
public int getJ(a) {
returnj; }}public class SyncMain {
public static void main(String[] agrs) throws InterruptedException {
SyncTest syncTest = new SyncTest();
Thread thread = new Thread(() -> syncTest.incr());
Thread threadTwo = new Thread(() -> syncTest.incr());
thread.start();
threadTwo.start();
thread.join();
threadTwo.join();
// The final print is 20000. If synchronized is not used, thread-safety issues will occur and the output is uncertainSystem.out.println(syncTest.getJ()); }}Copy the code
The code is very simple, incr function modified by synchronized, the function logic is to sum J for 10000 times, two threads execute incr function, and finally output J result.
Is decorated synchronized function we referred to as “synchronization function, thread execution said before the synchronization function, need to get the monitor lock, hereinafter referred to as lock, acquiring a lock is successful to perform synchronization function, after the synchronization function, the thread will release the lock and notify awaken other threads for locks, failed to get the lock” is blocked and wait for a notice awaken this thread to get lock “, The synchronization function takes this as the lock, which is the current object. For example, the syncTest object is shown in the above code snippet.
- thread
thread
performsyncTest.incr()
before - thread
thread
Obtaining the lock successfully - thread
threadTwo
performsyncTest.incr()
before - thread
threadTwo
Failed to obtain the lock - thread
threadTwo
Block and wait to wake up - thread
thread
aftersyncTest.incr()
.j
Accumulated to10000
- thread
thread
Release lock, wake up notificationthreadTwo
Thread acquisition lock - thread
threadTwo
Obtaining the lock successfully - thread
threadTwo
aftersyncTest.incr()
.j
Accumulated to20000
- thread
threadTwo
Release the lock
Static functions
A static function, as its name implies, is a static function that uses the same Synchronized approach as a normal function. The only difference is that the object is not this, but Class.
The code snippet for multithreaded Synchronized modifiers is shown below.
public class SyncTest {
private static int j = 0;
/** * increment method */
public static synchronized void incr(a){
// Critical section code --start
for (int i = 0; i < 10000; i++) {
j++;
}
// Critical section code --end
}
public static int getJ(a) {
returnj; }}public class SyncMain {
public static void main(String[] agrs) throws InterruptedException {
Thread thread = new Thread(() -> SyncTest.incr());
Thread threadTwo = new Thread(() -> SyncTest.incr());
thread.start();
threadTwo.start();
thread.join();
threadTwo.join();
// The final print is 20000. If synchronized is not used, thread-safety issues will occur and the output is uncertainSystem.out.println(SyncTest.getJ()); }}Copy the code
Java static resources can be directly called by the Class name, static resources do not belong to any instance object, it only belongs to the Class object, each Class has only one Class object in J V M, so the synchronous static function will use the Class object as the lock, subsequent lock acquisition, lock release process is the same.
The code block
Both ordinary functions and static functions introduced above have relatively large granularity, and the scope of the whole function is locked. Now, if you want to narrow the scope and configure it flexibly, you need to use code blocks, and use {} symbols to define the scope to Synchronized modification.
The following code defines syncDbData, which is a pseudo-synchronous data function. It takes 2 seconds, and the logic does not involve reading or writing shared resources (non-critical section). Synchronized is only used in different postures, one for functions and the other for code blocks.
public class SyncTest {
private static int j = 0;
/** * Synchronize library data, time-consuming, code resources do not involve shared resource read and write operations. * /
public void syncDbData(a) {
System.out.println("Db data synchronization starts ------------");
try {
// Synchronization takes 2 seconds
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Db data synchronization completed ------------");
}
// Increment method
public synchronized void incr(a) {
//start-- critical section code
// Synchronize library data
syncDbData();
for (int i = 0; i < 10000; i++) {
j++;
}
//end-- critical section code
}
// Increment method
public void incrTwo(a) {
// Synchronize library data
syncDbData();
synchronized (this) {
//start-- critical section code
for (int i = 0; i < 10000; i++) {
j++;
}
//end-- critical section code}}public int getJ(a) {
returnj; }}public class SyncMain {
public static void main(String[] agrs) throws InterruptedException {
//incr synchronizes method execution
SyncTest syncTest = new SyncTest();
Thread thread = new Thread(() -> syncTest.incr());
Thread threadTwo = new Thread(() -> syncTest.incr());
thread.start();
threadTwo.start();
thread.join();
threadTwo.join();
// The final print is 20000
System.out.println(syncTest.getJ());
//incrTwo Synchronized block execution
thread = new Thread(() -> syncTest.incrTwo());
threadTwo = new Thread(() -> syncTest.incrTwo());
thread.start();
threadTwo.start();
thread.join();
threadTwo.join();
// The final print is 40000System.out.println(syncTest.getJ()); }}Copy the code
SyncDbData () can be executed concurrently or concurrently with multiple threads. Synchronization is a Synchronized Synchronized method that can be executed concurrently or simultaneously with multiple threads.
We use code blocks to narrow the scope, define the right critical section, and improve performance. We turn our attention to incrTwo synchronous block execution. IncrTwo synchronizes by modifying the code block, locking only the self-increasing code segment.
In addition to flexible control range, Synchronized () can also work cooperatively between threads. Since Synchronized () brackets can receive any Object as a lock, it can use wait, notify, notifyAll and other functions of Object. Synchronized is not recommended because the LockSupport utility class is a better choice.
- Wait: The current thread pauses to release the lock
- Notify: Release the lock and wake up the thread that called WAIT (if there are more than one threads)
- NotifyAll: Releases the lock and wakes up all the threads that have called wait
Principle of Synchronized
public class SyncTest { private static int j = 0; /** * Synchronize library data, time-consuming, code resources do not involve shared resource read and write operations. * / public void syncDbData () {System. Out. Println (" db data start synchronization -- -- -- -- -- -- -- -- -- -- -- -- "); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } system.out. println("db data synchronization completed ------------"); } public synchronized void incr() {//start-- synchronized void incr(); for (int i = 0; i < 10000; i++) { j++; Public void incrTwo() {// syncDbData(); Synchronized (this) {//start-- start for (int I = 0; i < 10000; i++) { j++; }} public int getJ() {return j; }}Copy the code
In order to explore the principle of Synchronized, we decompilate the above code, output the decompilated results, and see how the underlying implementation (environment Java 11, Win 10 system).
Only incr and incrTwo functions are interceptedpublic synchronized void incr(a);
Code:
0: aload_0
1: invokevirtual #11 // Method syncDbData:()V
4: iconst_0
5: istore_1
6: iload_1
7: sipush 10000
10: if_icmpge 27
13: getstatic #12 // Field j:I
16: iconst_1
17: iadd
18: putstatic #12 // Field j:I
21: iinc 1.1
24: goto 6
27: return
public void incrTwo(a);
Code:
0: aload_0
1: invokevirtual #11 // Method syncDbData:()V
4: aload_0
5: dup
6: astore_1
7: monitorenter / / acquiring a lock
8: iconst_0
9: istore_2
10: iload_2
11: sipush 10000
14: if_icmpge 31
17: getstatic #12 // Field j:I
20: iconst_1
21: iadd
22: putstatic #12 // Field j:I
25: iinc 2.1
28: goto 10
31: aload_1
32: monitorexit // Exit properly to release the lock
33: goto 41
36: astore_3
37: aload_1
38: monitorexit // Asynchronously exit to release the lock
39: aload_3
40: athrow
41: return
Copy the code
Ps: If you are interested in the above instructions, please Google “JVM bytecode Instructions table”.
IncrTwo is block synchronization. In the decompile result, we find monitorenter and Monitorexit directives (acquire and release locks).
Monitorenter inserts at the beginning of the block and Monitorexit at the end of the block. J V M needs to ensure that each Monitorenter has a Monitorexit corresponding to it.
Any object is associated with a monitor lock, and the thread attempts to take ownership of the Monitor when executing the Monitorenter instruction.
- if
monitor
The entry number of is0
, the thread entersmonitor
And then set the entry number to1
, the thread ismonitor
The owner of the - If the thread already owns it
monitor
, re-enter, thenmonitor
The number of entries plus1
- Threads execute
monitorexit
.monitor
The number of entries to -1, how many times has it been executedmonitorenter
, and finally execute the corresponding number of timesmonitorexit
- If another thread is already occupied
monitor
, the thread enters the blocking state untilmonitor
Is 0, then try again to obtainmonitor
The ownership of the
If you look back at incr, incr is synchronized as a normal function. Although the monitorenter and Monitorexit directives are not seen in the decomcompiled results, the actual flow is the same as incrTwo, executed by Monitor, but in an implicit way. Finish with a flow chart.
Synchronized optimization
Various tweaks have been made to the Synchronized keyword since Jdk 1.5, and Synchronized has become faster than it used to be, which is why the official recommendation to use Synchronized is as follows.
- Lock coarsening
- Lock elimination
- Lock escalation
Lock coarsening
The critical range of mutual exclusion should be kept as small as possible to minimize the number of synchronized operations, reduce the blocking time and, if there is a lock contention, allow the thread waiting for the lock to acquire the lock as quickly as possible.
However, locking and unlocking also consumes resources. A series of consecutive locking and unlocking operations may cause unnecessary performance loss. Lock coarsening is to “connect multiple consecutive locking and unlocking operations together” to expand a wider range of locks, avoiding frequent locking and unlocking operations.
J V M detects that a series of operations lock the same object (J ++ for 10000 times, no lock coarser 10,000 times), then J V M coarses the range of the lock to the outside of the series of operations (such as outside of the for loop), The sequence of operations requires only one lock.
Lock elimination
(when the Java virtual machine in the JIT compiler can be simple to understand for the first time a piece of code will be executed when compile, also known as instant compiled), run through the context of the scan, after the escape analysis (object is used in the function, may also be referenced external function, called the escape function) and remove lock there can be no Shared resource competition, Eliminating unnecessary locks in this way saves meaningless time.
The code uses Object as the lock, but the Object’s life cycle is only in the incrFour() function and is not accessed by other threads, so it is optimized out during compilation (where Object is not an escaped Object).
Lock escalation
In Java, every object has an object header, which consists of a Mark World, a pointer to a class, and an array length. For this article, we only need to care about Mark World, which records the object’s HashCode, generational age, and lock flag bits.
Mark World simplified structure
The lock state | Store content | Lock tag |
---|---|---|
unlocked | Object hashCode, object generation age, whether biased lock (0) | 01 |
Biased locking | Biased thread ID, biased timestamp, object generation age, whether biased lock (1) | 01 |
Lightweight lock | Pointer to the lock record in the stack | 00 |
Heavyweight lock | A pointer to a mutex (heavyweight lock) | 10 |
Readers need to know that lock upgrades are reflected in the Mark World section of the lock object header, meaning that the contents of the Mark World will change as the lock is upgraded.
Java1.5 introduced biased locking and lightweight locking in order to reduce the performance cost of acquiring and releasing locks. Synchronized upgrades are in the order of “no lock > biased locking > lightweight locking > heavyweight locking.
Biased locking
In most cases, the lock is always acquired multiple times by the same thread, and there is no multi-thread competition, so biased locking appears. Its goal is to reduce the consumption of acquiring locks and improve performance when only one thread executes synchronized code blocks (biased locking can be turned off by J V M parameter: -xx: -usebiasedLocking =false, the program enters the lightweight locking state by default.
Before a thread executes a synchronized code or method, it only needs to check whether the thread ID in the Mark Word of the object header is the same as the current thread ID. If so, it directly executes the synchronized code or method. The specific process is as follows
- No lock state, whether the stored content is biased lock (
0
) “, lock identifier bit01
CAS
Sets the current thread ID toMark Word
Store contents- Whether the lock is biased
0
=> Whether the lock is biased1
- Execute synchronized code or methods
- Biased lock state, store content “whether biased lock (
1
), thread ID “, lock identifier bit01
- Contrast thread
ID
Consistent, if synchronous code or methods are executed consistently, otherwise enter the following flow - If not,
CAS
willMark Word
The threadID
Set to the current threadID
, execute the synchronized code or method, otherwise enter the following process CAS
If the setting fails, it proves that there is multi-thread competition, triggering the cancellation of biased lock. When the global safety point is reached, the thread of biased lock is suspended, and the biased lock is upgraded to lightweight lock, and then the execution continues at the safe point.
- Contrast thread
Lightweight lock
Lightweight locks consider scenarios where there are not many threads competing for the lock object and the lock is held for a short time. If the lock is released shortly after the block, the cost is not worth the cost. Therefore, do not block the thread and let it spin for some time to wait for the lock to be released.
When the current thread holds a biased lock, it is accessed by another thread, and the biased lock is upgraded to a lightweight lock. Other threads will try to acquire the lock through the form of spin without blocking, thus improving performance. There are two main situations for obtaining lightweight locks: (1) when the biased lock function is turned off; (2) Multiple threads compete for biased locks, so biased locks are upgraded to lightweight locks.
- No lock state, whether the stored content is biased lock (
0
) “, lock identifier bit01
- When bias locking is turned off
CAS
Sets the pointer to the lock record in the current thread stackMark Word
Store content- The lock identifier bit is set to
00
- Execute synchronized code or methods
- When you release the lock, return it
Mark Word
content
- Lightweight lock state, store content “thread stack lock record pointer”, lock identifier bit
00
(Threads that store content are “threads that hold lightweight locks.”)CAS
Sets the pointer to the lock record in the current thread stackMark Word
Store content, set successful lightweight lock acquisition, execute synchronized block code or method, otherwise execute the following logic- Set failure, proving that there is a certain amount of competition in multi-threading, thread spin step operation, spin a certain number of times or failed, lightweight lock upgrade to heavyweight lock
Mark Word
The stored content is replaced by the heavyweight lock pointer, lock marker bit10
Heavyweight lock
Once lightweight locks swell, they are upgraded to heavyweight locks, which rely on the operating system’s MutexLock to implement and need to move from user to kernel mode, which is very expensive, which is why Synchronized was inefficient before Java1.6.
When upgrading to a heavyweight lock, the status value of the lock flag bit changes to 10. At this time, the content stored in Mark Word is the pointer to the heavyweight lock, and the threads waiting for the lock will enter the blocking state. The following is a simplified version of the lock upgrade process.
Good historical articles recommended
- From shallow to deep CAS, Xiao Bai can also align with BAT interviewers
- Small white also can understand the Java memory model
- Nanny Level teaching, 22 images uncover ThreadLocal
- Can process, thread and coroutine be confused? A penny for you to know!
- What is thread safety? This article will give you insight
About me
Here is A star, a Java program ape who loves technology. The public account “program ape A Star” will regularly share the operating system, computer network, Java, distributed, database and other high-quality original articles. 2021, grow together with you on the road of Be Better! .
Thank you very much for your little brothers and sisters to see here, the original is not easy, the article can be helpful to pay attention to, point a like, share and comment, are support (don’t want white whoring)!
May you and I both go where we want to go. See you in the next article