One, foreword

Synchronized is used for thread synchronization, I believe we all know, but how to ensure the specific thread synchronization, what are the requirements? That’s what we’re going to talk about today.

What does Synchronized do

First, we need to clarify a few premises:

  • There are multiple threads running;
  • Both need to access and operate on the same object (or resource);

Note:

  • There is no such thing as synchronization if different threads access different instances of the same type!
  • Note on the previous point: static method synchronization for multi-threaded operations is the synchronization of property classes (class locks)

So, there are two types of locks involved: “class locks” and “object locks”.

Let’s look at an example (multiple threads accessing the same object without adding any synchronization measures)

public class Main { private int ticket = 100; public void decrease() { ticket --; System.out.println("after tickets = " + ticket + ", " + Thread.currentThread().getName()); } public static void main(String[] args) { Main ticket = new Main(); for (int i = 0; i < 5; i ++) { new Thread(() -> { try { Thread.sleep(0); } catch (InterruptedException e) { e.printStackTrace(); } ticket.decrease(); }).start(); }}}Copy the code

Output log:

after tickets = 98, Thread-0
after tickets = 96, Thread-3
after tickets = 95, Thread-4
after tickets = 97, Thread-2
after tickets = 98, Thread-1
Copy the code

We found a problem: Color {red}{Data did not decrease in the order we expected (e.g., thread-0 / thread-1) )} The data did not decrease as we expected (e.g., Thread−0/Thread−1);

If we add the keyword “synchronized” to “Decrease”, then look at the result:

public class Main { private int ticket = 100; public synchronized void decrease() { ticket --; System.out.println("after tickets = " + ticket + ", " + Thread.currentThread().getName()); } public static void main(String[] args) { Main ticket = new Main(); for (int i = 0; i < 5; i ++) { new Thread(() -> { try { Thread.sleep(0); } catch (InterruptedException e) { e.printStackTrace(); } ticket.decrease(); }).start(); }}}Copy the code

Output log:

after tickets = 99, Thread-0
after tickets = 98, Thread-3
after tickets = 97, Thread-2
after tickets = 96, Thread-1
after tickets = 95, Thread-4
Copy the code

The threads are still executing in random order, but at least the data is correct.

C. Synchronized d. Synchronized

From the previous section, we had a preliminary understanding of the role of Syncrhonized. In this section, we specifically talked about Synchronized.

Java provides a built-in locking mechanism to support atomicity, visibility, orderliness, and reentrancy: Synchronized blocks; The synchronization code block consists of two parts:

  • An object reference to the lock;
  • Lock protected code block;

Each Java object can be used to implement a synchronous Lock, called a Intrinsic Lock or Monitor Lock. The thread will automatically attempt to acquire the lock before entering the synchronized block. Once it has the lock, it will enter the synchronized block and release the lock when exiting the synchronized block (either normal return or abnormal exit).

3.1 Synchronized

3.1.1 atomicity

One or more operations, either all performed and performed without interruption by any factor; Or none at all.

In Java, the read and assignment operations of primitive datatype variables are atomic operations that cannot be broken and are either performed or not performed. However, operations such as “I ++” and “I += 1” are not atomic. They are divided into three steps: read, calculate, and assign. The original value may have been assigned before these steps are completed.

All operations of Synchronized modified classes or objects are atomic, because before execution, the lock of the class or object must be acquired before execution, and after execution, it needs to be released. The execution process cannot be interrupted, that is, atomicity is guaranteed.


Note: S y n c h r o n i z e d with v o l a t i l e The big difference is atomicity, v o l a t i l e Not atomic! \color{red}{note: The main difference between Synchronized and volatile is atomicity. Volatile does not have atomicity! }

3.1.2 visibility

When multiple threads access the same resource, the state, value information, and so on of the resource are visible to other threads.

Both Synchronized and volatile have visibility. When Synchronized locks a class or object, a thread must acquire the lock in order to access the class or object. The lock status is visible to all other threads. And before releasing the lock, changes to the variable will be flushed to the main memory to ensure the visibility of the resource variable. If a thread occupies the lock, other threads must wait in the lock pool for the release of the lock.

The implementation of volatile is similar. The main memory of volatile variables is updated whenever their values need to be changed. Main memory is shared and visible to all threads, thus ensuring that variables read by other threads are always the most recent and visible.

3.1.3 Orderliness

The order of execution is based on code sequence.

Both Synchronized and volatile are ordered. Java allows the compiler and processor to reorder instructions, but reordering does not affect the order of a single thread, but rather the order of concurrent execution by multiple threads. Synchronized ensures that only one thread accesses the Synchronized code block at each moment, which also determines that the thread executes the Synchronized code block sequentially and ensures the orderliness.

3.1.4 reentrancy

Synchronized and ReentrantLock are reentrant locks. When a thread attempts to manipulate a critical resource for an object lock held by another thread, it blocks, but when a thread requests to hold the critical resource for an object lock again, it is a reentrant lock. In layman’s terms, a thread that owns a lock can still apply for the lock again.

3.2 Role of Synchronized

With all that said above, it can be summarized as follows:

  • Ensure that threads are mutually exclusive;
  • Ensure that only one thread is currently available;
  • Ensure that shared resources are modifiable (atomic) and visible;
  • Effectively solve the reordering problem;

3.3 Synchronized

  • General method of modification
  • Modify code block
  • Decorated static methods

Iv. Actual combat demonstration

4.1. Multi-threading + unsynchronized method calls

public class SyncTest { public void first() { System.out.println(System.currentTimeMillis() + " first --- in"); try { System.out.println(System.currentTimeMillis() + " first --- exec"); Thread.sleep(2000); } catch (Exception e) { } System.out.println(System.currentTimeMillis() + " first --- out"); } public void second() { System.out.println(System.currentTimeMillis() + " second --- in"); try { System.out.println(System.currentTimeMillis() + " second --- exec"); Thread.sleep(1000); } catch (Exception e) { } System.out.println(System.currentTimeMillis() + " second --- out"); } public static void main(String[] args) { SyncTest test = new SyncTest(); for (int i = 0; i < 2; i ++) { final int idx = i; new Thread(() -> { if (idx == 0) { test.first(); } else { test.second(); } }).start(); }}}Copy the code

The result is as follows:

1612098570725 first --- in
1612098570726 first --- exec
1612098570725 second --- in
1612098570726 second --- exec
1612098571727 second --- out
1612098572729 first --- out
Copy the code

We can see that the two threads are executing concurrently because “sleep for a second in the second method”, so the line that started execution last completes first.

4.2. Multi-threading + synchronous method call

Modify the above code slightly as follows:

public class SyncTest { public synchronized void first() { System.out.println(System.currentTimeMillis() + " first --- in"); try { System.out.println(System.currentTimeMillis() + " first --- exec"); Thread.sleep(2000); } catch (Exception e) { } System.out.println(System.currentTimeMillis() + " first --- out"); } public synchronized void second() { System.out.println(System.currentTimeMillis() + " second --- in"); try { System.out.println(System.currentTimeMillis() + " second --- exec"); Thread.sleep(1000); } catch (Exception e) { } System.out.println(System.currentTimeMillis() + " second --- out"); } public static void main(String[] args) { SyncTest test = new SyncTest(); for (int i = 0; i < 2; i ++) { final int idx = i; new Thread(() -> { if (idx == 0) { test.first(); } else { test.second(); } }).start(); }}}Copy the code

The result is as follows:

1612098877346 first --- in
1612098877346 first --- exec
1612098879350 first --- out
1612098879350 second --- in
1612098879350 second --- exec
1612098880355 second --- out
Copy the code

We see that, in contrast to the previous example, the second thread must wait until the first thread completes its execution before entering and executing.

4.3 Multi-threading + synchronous code block call

public class SyncTest { public void first() { System.out.println(System.currentTimeMillis() + " first --- in"); try { synchronized (this) { System.out.println(System.currentTimeMillis() + " first --- exec"); Thread.sleep(2000); } } catch (Exception e) { } System.out.println(System.currentTimeMillis() + " first --- out"); } public void second() { System.out.println(System.currentTimeMillis() + " second --- in"); try { synchronized (this) { System.out.println(System.currentTimeMillis() + " second --- exec"); Thread.sleep(1000); } } catch (Exception e) { } System.out.println(System.currentTimeMillis() + " second --- out"); } public static void main(String[] args) { SyncTest test = new SyncTest(); for (int i = 0; i < 2; i ++) { final int idx = i; new Thread(() -> { if (idx == 0) { test.first(); } else { test.second(); } }).start(); }}}Copy the code

The result is as follows:

1612099286790 first --- in
1612099286790 first --- exec
1612099286790 second --- in
1612099288795 second --- exec
1612099288795 first --- out
1612099289800 second --- out
Copy the code

We see that thread 2 enters the “second” method, but waits for thread 1 to finish executing the synchronized block before starting execution.

4.4 Multithreading + static method (class) synchronization

public class SyncTest { public static synchronized void first() { System.out.println(System.currentTimeMillis() + " first --- in"); try { System.out.println(System.currentTimeMillis() + " first --- exec"); Thread.sleep(2000); } catch (Exception e) { } System.out.println(System.currentTimeMillis() + " first --- out"); } public static synchronized void second() { System.out.println(System.currentTimeMillis() + " second --- in"); try { System.out.println(System.currentTimeMillis() + " second --- exec"); Thread.sleep(1000); } catch (Exception e) { } System.out.println(System.currentTimeMillis() + " second --- out"); } public static void main(String[] args) { SyncTest test1 = new SyncTest(); SyncTest test2 = new SyncTest(); for (int i = 0; i < 2; i ++) { final int idx = i; new Thread(() -> { if (idx == 0) { test1.first(); // synctest.first ()} else {test2.second(); // synctest.second ()}}).start(); }}}Copy the code

The result is as follows:

1612099702500 first --- in
1612099702500 first --- exec
1612099704503 first --- out
1612099704503 second --- in
1612099704503 second --- exec
1612099705505 second --- out
Copy the code

The synchronization of static methods is essentially class synchronization, because static methods belong to classes and cannot exist independently. So even though “test1” and “test2” are different objects, they both belong to the “SyncTest class,” and each class has a default lock called a “class lock.”

5. Principle of Synchronized

In this section, we will explain each of the synchronization examples above.

5.1. Static vs. non-static:

Static vs. non-static: Static vs. non-static: static vs. non-static

  • Methods and members that are static are classed and accessible to all objects of that class.
  • Non-static methods and members are owned by classed instantiated objects and can only be accessed by instantiated objects.

This is why static methods cannot access non-static methods or members.

5.2. Synchronize code blocks

public class SyncTest { public void first() { synchronized (this) { System.out.println(); }}}Copy the code

After compiling, we use the command:

javap -v SyncTest.class

See the compiled Class content:

Classfile/Users/qingye/Desktop/Java/Demo/out/production/Demo/com/Chris/test/SyncTest class Last modified on January 31, 2021. size 573 bytes SHA-256 checksum e5340e14a3844276f0f11d0b7ce4e3169bfd99c6ffcd68dbfeb0d228c7f764ac Compiled from "SyncTest.java" public class com.chris.test.SyncTest ...... {... public void first(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: Monitorenter / / key 1 4: getstatic # 2 / / Field Java/lang/System. Out: Ljava/IO/PrintStream; 7: invokevirtual # 3 / Java/Method/IO/PrintStream. 10: println () V aload_1 11: monitorexit / / key 2 12: goto 20 15: Astore_2 16: ALOAD_1 17: Monitorexit // Focus 3 18: ALOad_2 19: athrow 20: return...... } SourceFile: "SyncTest.java"Copy the code

If we focus only on “priorities 1, 2, 3” above, we will see that there are three directives, two of which are identical, which are of two types: “Monitorenter” and “Monitorexit”. These two directives are explained in the JVM specification:

  • monitorenter

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

  • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
  • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
  • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

Here’s what it means:

  • Each object has a “monitor lock,” which is locked when the monitor is occupied. The following happens when a thread attempts to acquire monitor ownership while executing the monitorenter directive:
  • If monitor is 0, the thread enters monitor and sets it to 1, and the thread is the owner of Monitor.
  • If the thread already owns monitor, when it enters monitor again, monitor increments by 1;
  • If another thread already owns monitor, that thread blocks until monitor is 0, and then attempts to acquire monitor ownership;
  • monitorexit

The thread that executes monitorexit must be the owner of the monitor associated with the

instance referenced by objectref.

The thread decrements the entry count of the monitor associated with objectref. If as a result

the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

Here’s what it means:

The thread executing Monitorexit must be the owner of Monitor. When the thread executes this instruction, the number of monitors is reduced by one. If monitor is 0, the thread exits monitor and no longer owns it. Other threads that are blocking can try to acquire the Monitor ownership.

Based on the above information, we know the underlying principle of Synchronized:

  • During compilation, Java inserts “Monitorenter” and “Monitorexit” instructions before and after the code block wrapped by Synchronized.
  • The JVM monitors all threads that execute a synchronized code block through a Monitor object, and is allowed to execute code within a synchronized code block by competing for monitor ownership.
  • Wait/notify depend on the monitor, which is why only calling wait in the synchronized code block/notify method, otherwise will be thrown. Java lang. IllegalMonitorStateException exception;


Why are there two m o n i t o r e x i t ? \color{red}{why are there two Monitorexit? }

11: Monitorexit // Focus 2 12: goto 20 // Skip to line 20 15: astore_2 16: ALOAD_1 17: Monitorexit // Focus 3 18: ALOad_2 19: athrow // throw exception 20: returnCopy the code

Normal execution (with no exceptions) would goto line 12, then goto would goto line 20, and return would exit. However, when we do get an exception, instead of executing line 11/12 normally, we do what follows, which is exception handling. As we said, monitorenter and MonitoreXit must correspond one by one. If the Monitorexit is not inserted without exception, other blocked threads will never be able to enter execution (Monitorexit subtracts Monitor by 1).

5.3. Common synchronization methods

public class SyncTest { public synchronized void first() { System.out.println(); }}Copy the code

Execute javap to view the compiled Class information

. public synchronized void first(); Descriptor: ()V flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED // Focus Code: stack=1, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: invokevirtual #3 // Method java/io/PrintStream.println:()V 6: return LineNumberTable: line 5: 0 line 6: 6 LocalVariableTable: Start Length Slot Name Signature 0 7 0 this Lcom/chris/test/SyncTest; } SourceFile: "SyncTest.java"Copy the code

We don’t find monitorenter and Monitorexit, but there is an additional modifier: the ACC_SYNCHRONIZED flag. The JVM synchronizes methods based on this flag: when a method is called, it checks for the presence of this flag, and if so, the executing thread attempts to acquire monitor before executing the method body, and then releases monitor. This is done implicitly without the need to insert bytecode.

5.4 Static method (class) synchronization

The underlying implementation is exactly the same as 5.2, except that one is a normal method (based on object locks) and one is a static method (based on class locks), but both are implicitly implemented by Monitor.

. public static synchronized void first(); Descriptor: ()V flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED // Focus Code: stack=1, locals=0, args_size=0 getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: invokevirtual #3 // Method java/io/PrintStream.println:()V 6: return LineNumberTable: line 5: 0 line 6: 6 } SourceFile: "SyncTest.java"Copy the code

Six, summarized

Synchronized is one of the most common and simplest thread-safe measures in Java concurrent programming. In this article, we hope to introduce you to Synchronized as well as monitor/JVM. It is also expected that you can further study and better understand the mechanism of concurrent programming.

There are alternatives to Synchronized, which I’ll share later.