synchronized关键字详解
一、synchronized的使用
官方文档对其解释是,synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都将通过同步的方式来进行。
synchronized的使用方式有三种:
- 修饰静态方法,对class进行加锁
- 修饰实例方法,对方法所属对象进行加锁
- 修饰代码块,对synchornized( )括号内对象加锁
1 public class SynchornizedTest { 2 3 public synchronized static void test_1(){ 4 System.out.println("静态方法"); 5 } 6 7 public synchronized void test_2(){ 8 System.out.println("实例方法"); 9 } 10 11 public void test_3(){ 12 synchronized (this) { 13 System.out.println("代码块"); 14 } 15 } 16 }
二、synchronized底层原理
由使用方法,synchronized的同步方式是通过锁实现,那么这个锁是如何实现的?对class加锁和对对象加锁有不同吗?
2.1 锁实现
Java虚拟机规范中原文,Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用Monitor来支持的。
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先持有Monitor,然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放Monitor。在方法执行期间,执行线程持有了Monitor,其他任何线程都无法再获得同一个Monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
同步一段指令集序列通常是由Java语言中的synchronized块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要编译器与Java虚拟机两者协作支持。
Java虚拟机中的同步(Synchronization)基于进入和退出Monitor对象实现。无论是显式同步(有明确的monitorenter和monitorexit指令)还是隐式同步(依赖方法调用和返回指令实现的)都是如此。
编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须有执行其对应monitorexit指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行monitorexit指令。
2.1.1 对象头
锁是加在对象上的,无论这个对象是类对象还是实例对象,而对象由三部分组成,对象头,实例数据和对齐填充。synchronized使用的锁对象是存储在Java对象头里的,而对象头结构是由Mark Word 和 Class Metadata Address 组成(对象是数组还有数组长度描述)。
Mark Word:存储对象的hashcode、锁信息、分代年龄和GC标志等信息
Class Metadata Address:类型指针,指向对象的类元数据,JVM通过这个指针确定该对象是哪个类到的实例
64位虚拟机中Mark Word结构:
synchronized属于结构中的重量级锁,锁标识位为10,其中指针指向的是monitor对象的起始地址。每个对象都与一个monitor相关联,一个monitor的lock的锁只能被一个线程在同一时间获得,在同步代码块中,JVM通过monitorenter和monitorexist指令实现同步锁的获取和释放功能。
2.1.2 monitor监视器
在HotSpot虚拟机中,monitor是由ObjectMonitor实现的,其结构为:
1 ObjectMonitor() { 2 3 _header = NULL; 4 5 _count = 0; // 计数器 6 7 _waiters = 0; 8 9 _recursions = 0; // 递归次数/重入次数 10 11 _object = NULL; 12 13 _owner = NULL; // 记录当前持有锁的线程ID 14 15 _WaitSet = NULL; // 等待池:处于wait状态的线程,会被加入到_WaitSet 16 17 _WaitSetLock = 0 ; 18 19 _Responsible = NULL ; 20 21 _succ = NULL ; 22 23 _cxq = NULL ; 24 25 FreeNext = NULL ; 26 27 _EntryList = NULL ; // 锁池:处于等待锁block状态的线程,会被加入到该列表 28 29 _SpinFreq = 0 ; 30 31 _SpinClock = 0 ; 32 33 OwnerIsThread = 0 ; 34 35 _previous_owner_tid = 0 36 } 37
结合上面的几种注释参数,整个底层流程为,每个等待锁的线程都会被封装成ObjectWaiter对象,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合(锁池)去竞争锁,当线程获取到对象的monitor 后进入 _owner 区域并把monitor中的owner变量设置为当前线程,_owner指向持有ObjectMonitor对象的线程。同时monitor中的计数器count加1。
若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合(等待池)中等待被唤醒。(这里就解释了为什么wait( )和notify( )要与同步方法一起使用,wait( )会释放掉monitor,而释放的前提是需要先获取到)。
这个过程体现在monitorenter和monitorexit中就比较简单了。
monitorenter:
在一个线程尝试获得与对象关联monitor的所有权时会发生如下的几件事情:
- 如果monitor的计数器为0,则意味着该monitor的lock还没有被获得,某个线程获得之后将立即对该计数器加一,该线程就是这个monitor的所有者了
- 如果一个已经拥有该monitor所有权的线程重入,则会导致monitor计数器再次累加
- 如果monitor已经被其他线程所拥有,则其他线程尝试获取该monitor的所有权时,会被陷入阻塞状态直到monitor计数器变为0,才能再次尝试获取对monitor的所有权
monitorexit:
释放对monitor的所有权,想要释放对某个对象关联的monitor的所有权的前提是,你曾经获得了所有权。释放monitor所有权的过程比较简单,就是将monitor的计数器减一,如果计数器的结果为0,那就意味着该线程不再拥有对该monitor的所有权,通俗地讲就是解锁。与此同时被该monitor block的线程将再次尝试获得对该monitor的所有权。
2.1.3 反编译class文件
根据原理剖析,我们对第一次测试代码进行反编译,得到常量池,同步方法,和同步代码块到的不同结果。
1 public class SynchornizedTest 2 minor version: 0 3 major version: 52 4 flags: ACC_PUBLIC, ACC_SUPER 5 Constant pool: 6 #1 = Methodref #8.#22 // java/lang/Object."<init>":()V 7 #2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; 8 #3 = String #25 // 静态方法 9 #4 = Methodref #26.#27 // java/io/PrintStream.println:(Ljava/lang/String;)V 10 #5 = String #28 // 实例方法 11 #6 = String #29 // 代码块 12 #7 = Class #30 // SynchornizedTest 13 #8 = Class #31 // java/lang/Object 14 #9 = Utf8 <init> 15 #10 = Utf8 ()V 16 #11 = Utf8 Code 17 #12 = Utf8 LineNumberTable 18 #13 = Utf8 test_1 19 #14 = Utf8 test_2 20 #15 = Utf8 test_3 21 #16 = Utf8 StackMapTable 22 #17 = Class #30 // SynchornizedTest 23 #18 = Class #31 // java/lang/Object 24 #19 = Class #32 // java/lang/Throwable 25 #20 = Utf8 SourceFile 26 #21 = Utf8 SynchornizedTest.java 27 #22 = NameAndType #9:#10 // "<init>":()V 28 #23 = Class #33 // java/lang/System 29 #24 = NameAndType #34:#35 // out:Ljava/io/PrintStream; 30 #25 = Utf8 静态方法 31 #26 = Class #36 // java/io/PrintStream 32 #27 = NameAndType #37:#38 // println:(Ljava/lang/String;)V 33 #28 = Utf8 实例方法 34 #29 = Utf8 代码块 35 #30 = Utf8 SynchornizedTest 36 #31 = Utf8 java/lang/Object 37 #32 = Utf8 java/lang/Throwable 38 #33 = Utf8 java/lang/System 39 #34 = Utf8 out 40 #35 = Utf8 Ljava/io/PrintStream; 41 #36 = Utf8 java/io/PrintStream 42 #37 = Utf8 println 43 #38 = Utf8 (Ljava/lang/String;)V
ConstantPool含基本类型和字符串及数组的常量值,还包含以文本形式出现的符号引用:类和接口的全限定名;字段的名称和描述符;方法和名称和描述符。
1 public static synchronized void test_1(); 2 descriptor: ()V 3 flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED 4 Code: 5 stack=2, locals=0, args_size=0 6 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 7 3: ldc #3 // String 静态方法 8 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 9 8: return 10 LineNumberTable: 11 line 4: 0 12 line 5: 8 13 14 public synchronized void test_2(); 15 descriptor: ()V 16 flags: ACC_PUBLIC, ACC_SYNCHRONIZED 17 Code: 18 stack=2, locals=1, args_size=1 19 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 20 3: ldc #5 // String 实例方法 21 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 22 8: return 23 LineNumberTable: 24 line 8: 0 25 line 9: 8
同步方法中并没有出现monitor,只有一个ACC_SYNCHRONIZED标记符。但实际上这里也是会用到monitorenter和monitorexit,只不过在修饰方法时用ACC_SYNCHRONIZED代替了。再加上ACC_STATIC就可以判断是静态方法还是实例方法了,后面拿到需要获取的所对象,实现方式就跟代码块相同了。
1 public void test_3(); 2 descriptor: ()V 3 flags: ACC_PUBLIC 4 Code: 5 stack=2, locals=3, args_size=1 6 0: aload_0 7 1: dup 8 2: astore_1 9 3: monitorenter 10 4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 11 7: ldc #6 // String 代码块 12 9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13 12: aload_1 14 13: monitorexit 15 14: goto 22 16 17: astore_2 17 18: aload_1 18 19: monitorexit 19 20: aload_2 20 21: athrow 21 22: return
同步代码块中有一个monitorenter和两个monitorexit,monitorenter和monitorexit是成对出现的,如果代码正常执行,最后会执行monitor正常释放锁,因此这里的两个monitorexit指令分别对应了正常释放和异常情况释放。
三、两个特别的monitor
上面我们得到,synchronized无论是修饰实例方法,静态方法,还是修饰代码块,实际上都是对对象加锁,只不过对象有类对象和实例对象。接下来就用代码进行验证:
3.1 this monitor
1 public class ThisMonitor { 2 3 public synchronized void method1() throws InterruptedException { 4 System.out.println(Thread.currentThread().getName()+" enter the method1"); 5 TimeUnit.SECONDS.sleep(40); 6 } 7 8 public synchronized void method2() throws InterruptedException { 9 System.out.println(Thread.currentThread().getName()+" enter the method2"); 10 TimeUnit.SECONDS.sleep(30); 11 } 12 13 public static void main(String[] args) throws InterruptedException { 14 final ThisMonitor thisMonitor = new ThisMonitor(); 15 new Thread("Thread_1"){ 16 @Override 17 public void run() { 18 try { 19 thisMonitor.method1(); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 } 24 }.start(); 25 26 TimeUnit.SECONDS.sleep(2); 27 new Thread("Thread_2"){ 28 @Override 29 public void run() { 30 try { 31 thisMonitor.method2(); 32 } catch (InterruptedException e) { 33 e.printStackTrace(); 34 } 35 } 36 }.start(); 37 } 38 } 39 40 41 //stack information 42 "Thread_2" #10 prio=5 os_prio=0 tid=0x15485400 nid=0x1854 waiting for monitor entry [0x1585f000] 43 java.lang.Thread.State: BLOCKED (on object monitor) 44 at day1.ThisMonitor.method2(ThisMonitor.java:13) 45 - waiting to lock <0x04d1bd00> (a day1.ThisMonitor) 46 at day1.ThisMonitor$2.run(ThisMonitor.java:35) 47 48 "Thread_1" #9 prio=5 os_prio=0 tid=0x02a79800 nid=0x3bc4 waiting on condition [0x157cf000] 49 java.lang.Thread.State: TIMED_WAITING (sleeping) 50 at java.lang.Thread.sleep(Native Method) 51 at java.lang.Thread.sleep(Thread.java:340) 52 at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) 53 at day1.ThisMonitor.method1(ThisMonitor.java:9) 54 - locked <0x04d1bd00> (a day1.ThisMonitor) 55 at day1.ThisMonitor$1.run(ThisMonitor.java:23)
同时修饰实例方法时,我们打印堆栈信息,Thread_2调用method2,进入了BLOCKED状态。把method2做一下修改,将其换成代码块的形式:
1 public void method2() throws InterruptedException { 2 synchronized (this) { 3 System.out.println(Thread.currentThread().getName() + " enter the method2"); 4 TimeUnit.SECONDS.sleep(30); 5 } 6 } 7 8 //stack information 9 "Thread_2" #10 prio=5 os_prio=0 tid=0x159aa800 nid=0x2158 waiting for monitor entry [0x15cff000] 10 java.lang.Thread.State: BLOCKED (on object monitor) 11 at day1.ThisMonitor.method2(ThisMonitor.java:14) 12 - waiting to lock <0x05306128> (a day1.ThisMonitor) 13 at day1.ThisMonitor$2.run(ThisMonitor.java:37) 14 15 "Thread_1" #9 prio=5 os_prio=0 tid=0x159a7800 nid=0x1aa4 waiting on condition [0x15c6f000] 16 java.lang.Thread.State: TIMED_WAITING (sleeping) 17 at java.lang.Thread.sleep(Native Method) 18 at java.lang.Thread.sleep(Thread.java:340) 19 at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) 20 at day1.ThisMonitor.method1(ThisMonitor.java:9) 21 - locked <0x05306128> (a day1.ThisMonitor) 22 at day1.ThisMonitor$1.run(ThisMonitor.java:25)
打印堆栈信息,Thread_2仍然进入了BLOCKED状态。可见synchronized同步实例方法,争抢的是同一个monitor,即实例对象的monitor。
3.2 class monitor
1 public class ThisMonitor { 2 3 public synchronized static void method1() throws InterruptedException { 4 System.out.println(Thread.currentThread().getName()+" enter the method1"); 5 TimeUnit.SECONDS.sleep(40); 6 } 7 8 public synchronized static void method2() throws InterruptedException { 9 System.out.println(Thread.currentThread().getName() + " enter the method2"); 10 TimeUnit.SECONDS.sleep(30); 11 } 12 } 13 14 15 16 //stack information 17 "Thread_2" #10 prio=5 os_prio=0 tid=0x15279400 nid=0x3f68 waiting for monitor entry [0x1565f000] 18 java.lang.Thread.State: BLOCKED (on object monitor) 19 at day1.ThisMonitor.method2(ThisMonitor.java:13) 20 - waiting to lock <0x04b1b5b0> (a java.lang.Class for day1.ThisMonitor) 21 at day1.ThisMonitor$2.run(ThisMonitor.java:35) 22 23 "Thread_1" #9 prio=5 os_prio=0 tid=0x15276400 nid=0x3a24 waiting on condition [0x155cf000] 24 java.lang.Thread.State: TIMED_WAITING (sleeping) 25 at java.lang.Thread.sleep(Native Method) 26 at java.lang.Thread.sleep(Thread.java:340) 27 at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) 28 at day1.ThisMonitor.method1(ThisMonitor.java:9) 29 - locked <0x04b1b5b0> (a java.lang.Class for day1.ThisMonitor) 30 at day1.ThisMonitor$1.run(ThisMonitor.java:23)
打印堆栈信息,Thread_2进入BLOCKED状态,因此两个静态方法竞争的也是同一个锁,与3.1的堆栈信息不同的是-locked是java.lang.Class。可见两者争抢的是class的monitor,同样地我们将method2换成代码块形式:
1 public static void method2() throws InterruptedException { 2 synchronized (ThisMonitor.class) { 3 System.out.println(Thread.currentThread().getName() + " enter the method2"); 4 TimeUnit.SECONDS.sleep(30); 5 } 6 } 7 8 //stack information 9 "Thread_2" #10 prio=5 os_prio=0 tid=0x157ab400 nid=0x4bdc waiting for monitor entry [0x15b8f000] 10 java.lang.Thread.State: BLOCKED (on object monitor) 11 at day1.ThisMonitor.method2(ThisMonitor.java:14) 12 - waiting to lock <0x05105528> (a java.lang.Class for day1.ThisMonitor) 13 at day1.ThisMonitor$2.run(ThisMonitor.java:37) 14 15 "Thread_1" #9 prio=5 os_prio=0 tid=0x01353c00 nid=0x4dc0 waiting on condition [0x15aff000] 16 java.lang.Thread.State: TIMED_WAITING (sleeping) 17 at java.lang.Thread.sleep(Native Method) 18 at java.lang.Thread.sleep(Thread.java:340) 19 at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) 20 at day1.ThisMonitor.method1(ThisMonitor.java:9) 21 - locked <0x05105528> (a java.lang.Class for day1.ThisMonitor) 22 at day1.ThisMonitor$1.run(ThisMonitor.java:25)
method2仍然陷入阻塞,可见两个静态方法抢夺的是类对象的monitor。
官方文档对此说明:因为静态方法与类关联,而不是与对象关联。在这种情况下,线程获取与类关联的类对象的内部锁。因此,对类的静态字段的访问是由一个锁控制的,该锁不同于类的任何实例的锁。