原链接:https://juejin.cn/post/6874196050446385165
synchronized,是用来解决并发情况下数据同步问题的关键字,讲人话就是,多线程情况下,被synchronized修饰的代码,在任何时间内都只能有一个线程在执行。下面将要介绍synchronized如何使用,synchronized是如何通过锁定对象来限制单个线程执行,synchronized是如何通过对象头联系上Monitor对象。最后介绍一下JDK1.6之后,引入的偏向锁,轻量级锁,重量级锁的概念。
synchronized用法
3种用法
- 同步实例方法,锁的是当前实例对象
- 同步类静态方法,锁是当前类对象
- 同步代码块,锁是括号里面的对象
public class SynchronizedTest { /** * 同步实例方法,锁实例对象 */ public synchronized void test() { } /** * 同步类方法,锁类对象 * 锁住的是SynchronizedTest.class类对象 */ public synchronized static void test1() { } /** * 同步代码块 */ public void test2() { String str = "ABC" // 锁实例对象 synchronized (str) { } } }
synchronized原理
从上面的介绍的用法可以看出,synchronized上锁都是需要锁定类对象或者实例对象
。所以先来介绍一下Java对象内存布局
。
java对象内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header),实例数据(Instance Data)和对象填充(Padding)。
实例数据:对象真正存储的有效信息,存放类的属性数据信息,包括父类的属性信息;
对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍
。填充数据不是必须存在的,仅仅是为了字节对齐。
重头戏是接下来的对象头
,对象头中包含了与锁关联起来的答案。
对象头:Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。
对象头
Class Pointer:是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
Mark Word : 用于存储对象自身的运行时数据,就是它里面存储了与锁关联起来的关键信息。
Mark Word
下图就是Mark Word存储地信息。先把重点放在重量锁一行,指向互斥量(重量级锁)指针一行。其他的后续介绍。重点就是弄明白个互斥量指针是个什么东西?
monitor对象
好了,视线继续挪回到上面提到的synchronized用法。
使用javap命令反编译
之前生成的SynchronizedTest.class
javap -c SynchronizedTest.class
复制代码
反编译后,得到以下字节码
Compiled from "SynchronizedTest.java" public class SynchronizedTest { public SynchronizedTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public void doSth(); Code: 0: ldc #2 // class SynchronizedTest 2: dup 3: astore_1 4: monitorenter // 重点 5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 8: ldc #4 // String test Synchronized 10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: aload_1 14: monitorexit // 重点 15: goto 23 18: astore_2 19: aload_1 20: monitorexit // 重点 21: aload_2 22: athrow 23: return Exception table: from to target type 5 15 18 any 18 21 18 any }
这里的重点是这两行monitorenter
和monitorexit
.
monitorenter:尝试获取上文提到的Mark Word重量级锁对象。实际上就是Monitor对象。
monitorexit: monitor的拥有者线程,执行同步代码完毕,计数器减1,如果计数器为0,表明该线程不再拥有monitor。其他线程就允许尝试去获得该monitor了。
之所以会有两个monitorexit,是因为正常流程执行完后 15 直接goto 23了。 下面的是考虑了异常时的Monitor对象的释放。
所以重点就是接下的监视器
监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
结论
synchronized关键字 通过 锁定 对象 ---> 对象头中的 Mark Work --> Mark Word中指向的Monitor对象 ---> 对应操作系统的一个Mutex Lock(互斥锁)。
最终实现与操作系统互斥锁关联。现在我们继续了解Monitor对象的的数据结构
monitor监视器源码是C++写的,在虚拟机的ObjectMonitor.hpp文件中。数据结构长这样
ObjectMonitor() { _header = NULL; _count = 0; // 记录个数 _waiters = 0, _recursions = 0; // 锁重入次数 _object = NULL; _owner = NULL; // 拥有持有ObjectMonitor的线程 _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
顺便解释一下 线程Waiting
和Blocked
状态的区别。synchronized会导致线程进入Blocked状态,Object.wait()导致线程进入Waiting状态,Waiting线程被其他线程调用Object.notify()唤醒之后,重新获取对象上的锁的时候也会进入Blocked状态。也就是说Waiting状态的线程是自己放弃时间片,但是Blocking状态的线程想拥有时间片,但是被没有获取到锁。
锁优化
JDK1.6版本对锁进行了优化,引入了偏向锁
和轻量级锁的概念
。下面先介绍一下这两种锁。
偏向锁
HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。所以才给起了个偏向锁
的名称。 当出现线程竞争时锁才会升级成轻量级锁。
偏向锁的获取过程
- 访问Mark Word(具体位置参考上图)中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
- 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
- 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果指向为空,竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
- 如果CAS获取偏向锁失败,则表示有竞争(CAS获取偏向锁失败说明至少有过其他线程曾经获得过偏向锁,因为线程不会主动去释放偏向锁)。
- 执行同步代码。
竞争失败后
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断持有锁的线程是否处于活动状态。如果挂了,撤销偏向锁后恢复到无锁(标志位为“01”)然后重新偏向新的线程, 如果还活着,就升级为轻量级锁(标志位为“00”)的状态。
大概流程图如下
偏向锁流程图偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。
轻量级锁
当线程竞争情况不严重时,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
轻量级锁升级
若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
获取过程
- 判断同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),如果是进入步骤(2),否则执行步骤(5)
- 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
- 拷贝对象头中的Mark Word复制到锁记录中。
- 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向锁记录的指针,并将锁记录里的owner指针指向object mark word。如果更新成功,表示竞争到锁,则执行同步代码,否则执行步骤(5)。
- 如果当前mark处于加锁状态,且mark中的ptr指针指向当前线程的栈帧,则执行同步代码,否则说明有多个线程竞争轻量级锁,若当前只有一个等待线程,则可通过自旋稍微等待一下,可能另一个线程很快就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁。
流程图
轻量级锁流程图区别
这里可以看出轻量级锁与偏向锁的区别主要在于这个锁记录(Lock Record),还有轻量级锁允许有轻微的竞争。
结语
之前一直不明白,Synchronize为何要锁住一个对象就能够同步代码块,以及无锁 -->偏向锁 --> 轻量级锁 --> 重量级锁之前的转换过程. 后面有时间的话还有 参看以下 ReentrantLock是如何实现锁的。