并发编程原理与实战(二十六)深入synchronized底层原理实现
锁在保证线程安全的过程中起着关键性的作用。在第二篇文章和第十五篇文章中,我们对锁以及synchronized关键字已经有了一定的了解。下面我们对synchronized的进步一学习,深入分析其底层的实现原理。
对象监视器锁
下面摘自官方文档关于对象监视器的描述:
The Java programming language provides multiple mechanisms for
communicating between threads. The most basic of these methods is
synchronization, which is implemented using monitors. Each object in Java is
associated with a monitor, which a thread can lock or unlock. Only one thread at
a time may hold a lock on a monitor. Any other threads attempting to lock that
monitor are blocked until they can obtain a lock on that monitor. A thread t may
lock a particular monitor multiple times; each unlock reverses the effect of one
lock operation.
java编程语言提供了多种线程间通信的机制,通过监视器实现的同步是最基础的一种方法,这个是实现线程间同步的核心机制。Java中每个对象都有一个关联的监视器对象,线程可以对该对象进行加锁(lock)和解锁(unlock)操作。同一时刻仅允许一个线程持有监视器对象,其他尝试锁定该监视器的线程将被阻塞,直至获取锁成功。一个线程可多次锁定同一监视器对象,每次解锁操作抵消一次锁定。
由此可见,基于监视器实现的锁具有独占性和可重入性(多次上锁)。而synchronized正是基于对象监视器实现的锁。
synchronized使用
同步语句
我们看看synchronized修饰代码块的作用。
The synchronized statement (§14.19) computes a reference to an object; it then
attempts to perform a lock action on that object's monitor and does not proceed
further until the lock action has successfully completed. After the lock action has
been performed, the body of the synchronized statement is executed. If execution
of the body is ever completed, either normally or abruptly, an unlock action is
automatically performed on that same monitor.
当执行 synchronized 修饰的语句块时,首先计算对象的引用,随后尝试在该对象的监视器上执行锁定操作,并在锁定成功后才继续执行后续代码。如果同步块内的代码执行完成,无论正常结束或异常中断,在该监视器上将会自动执行一个解锁操作。
同步方法
我们再看看synchronized修饰实例方法和静态方法的作用。
A synchronized method (§8.4.3.6) automatically performs a lock action when it is
invoked; its body is not executed until the lock action has successfully completed. If
the method is an instance method, it locks the monitor associated with the instance
for which it was invoked (that is, the object that will be known as this during
execution of the body of the method). If the method is static, it locks the monitor
associated with the Class object that represents the class in which the method is
defined. If execution of the method's body is ever completed, either normally or
abruptly, an unlock action is automatically performed on that same monitor.
一个synchronized修饰符的方法被调用时将会执行一个锁定操作,synchronized代码块中的代码不会执行直到锁定操作成功完成。如果方法是一个实例方法,锁定的是当前实例关联的监视器(即方法体内的 this 对象)。如果方法是一个静态方法,锁定的该方法所属类的 Class 对象关联的监视器。如果方法体执行完成,无论正常结束或异常中断,在该监视器上将会自动执行一个解锁操作。
基于上述语法说明,才有了我们常见的synchronized的三种用法,回顾下代码,如下:
public class SynchronizedTest {public static void main(String[] args) {System.out.println("synchronized class");}//修饰普通方法public synchronized void output(String name) {for(int i=0; i<5; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(name);}}//修饰代码块public void output2(String name) {synchronized(this) {for(int i=0; i<5; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(name);}}}//修饰静态方法public static synchronized void output3(String name) {for(int i=0; i<5; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(name);}}
}
死锁处理机制
在使用synchronized加锁时,开发者并不需要代码显示的控制死锁的问题。
The Java programming language neither prevents nor requires detection of
deadlock conditions. Programs where threads hold (directly or indirectly) locks
on multiple objects should use conventional techniques for deadlock avoidance,
creating higher-level locking primitives that do not deadlock, if necessary.
Other mechanisms, such as reads and writes of volatile variables and the use
of classes in the java.util.concurrent package, provide alternative ways of
synchronization.
Java编程语言既不阻止也不要求检测死锁现象。若线程直接或间接持有多个对象锁,开发者需采用以下策略:使用常规的死锁避免技术,必要时创建高级无死锁锁原语。替代同步方案的其他机制,比如volatile 变量的读写操作,java.util.concurrent 包中的并发工具类,提供了其他可选的同步方案。
底层实现原理
我们使用javap -c SynchronizedTest.class命令将上述代码对应的class文件反编译成字节码指令。
C:\Program Files\Java\jdk-21\bin>javap -verbose SynchronizedTest.class
...
{public com.demo.SynchronizedTest();descriptor: ()Vflags: (0x0001) ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 2: 0...public synchronized void output(java.lang.String);descriptor: (Ljava/lang/String;)Vflags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=2, locals=4, args_size=20: iconst_01: istore_22: iload_23: iconst_54: if_icmpge 347: ldc2_w #21 // long 1000l10: invokestatic #23 // Method java/lang/Thread.sleep:(J)V13: goto 2116: astore_317: aload_318: invokevirtual #31 // Method java/lang/InterruptedException.printStackTrace:()V21: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;24: aload_125: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V28: iinc 2, 131: goto 234: returnException table:from to target type7 13 16 Class java/lang/InterruptedExceptionLineNumberTable:line 8: 0line 10: 7line 13: 13line 11: 16line 12: 17line 14: 21line 8: 28line 16: 34StackMapTable: number_of_entries = 4frame_type = 252 /* append */offset_delta = 2locals = [ int ]frame_type = 77 /* same_locals_1_stack_item */stack = [ class java/lang/InterruptedException ]frame_type = 4 /* same */frame_type = 250 /* chop */offset_delta = 12public void output2(java.lang.String);descriptor: (Ljava/lang/String;)Vflags: (0x0001) ACC_PUBLICCode:stack=2, locals=6, args_size=20: aload_01: dup2: astore_23: monitorenter4: iconst_05: istore_36: iload_37: iconst_58: if_icmpge 4011: ldc2_w #21 // long 1000l14: invokestatic #23 // Method java/lang/Thread.sleep:(J)V17: goto 2720: astore 422: aload 424: invokevirtual #31 // Method java/lang/InterruptedException.printStackTrace:()V27: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;30: aload_131: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V34: iinc 3, 137: goto 640: aload_241: monitorexit42: goto 5245: astore 547: aload_248: monitorexit49: aload 551: athrow52: returnException table:from to target type11 17 20 Class java/lang/InterruptedException4 42 45 any45 49 45 any...public static synchronized void output3(java.lang.String);descriptor: (Ljava/lang/String;)Vflags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZEDCode:stack=2, locals=3, args_size=10: iconst_01: istore_12: iload_13: iconst_54: if_icmpge 347: ldc2_w #21 // long 1000l10: invokestatic #23 // Method java/lang/Thread.sleep:(J)V13: goto 2116: astore_217: aload_218: invokevirtual #31 // Method java/lang/InterruptedException.printStackTrace:()V21: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;24: aload_025: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V28: iinc 1, 131: goto 234: returnException table:from to target type7 13 16 Class java/lang/InterruptedException...
}
SourceFile: "SynchronizedTest.java"C:\Program Files\Java\jdk-21\bin>
从上面可以看出,synchronized修饰普通方法和修饰静态方法时,字节码文件方法中会有同步标志 ACC_SYNCHRONIZED,而修饰代码块时使用了生成显式的 monitorenter/monitorexit 指令。ACC_SYNCHRONIZED 是方法的访问标志位,编译时由编译器添加,无需生成显式的 monitorenter/monitorexit 指令。两种上锁和释放锁的方式是有区别的:
(1)当synchronized修饰的普通方法和修饰静态方法时方法被调用时,JVM 检查是否设置ACC_SYNCHRONIZED 标志,若设置了线程会隐式尝试获取锁:对于实例方法锁对象是当前实例(this);对于静态方法锁对象是当前类的 Class 对象(如 SynchronizedTest.class);获取锁失败时线程进入阻塞状态,直到锁被释放。锁释放时无论方法正常执行结束还是抛出异常,JVM 均自动释放锁避免死锁。
(2)当执行synchronized修饰的代码块时,底层通过monitorenter指令进行上锁,我们知道,每个java对象都有一个隐式关联一个监视器(锁),调用monitorenter 指令尝试获取该监视器的所有权;调用monitorexit 指令时释放锁,异常时也会触发释放避免死锁。