Synchronized实现原理
实现原理
- synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性;
- 用法:
- 实例方法,锁的是当前对象实例this
- 静态方法,锁的是类的Class对象实例
- 某个对象实例,锁的是该对象实例
- 原理:对象头+监视器锁(monitor)
Java对象头
-
组成部分:
-
Mark Word(标记字段):存储对象自身的运行时数据
-
哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等
-
Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间
-
Mark Word会随着程序的运行发生变化,变化状态如下(32位虚拟机)
-
-
Klass Pointer(类型指针):
- 对象指向它的类元数据的指针
-
Monitor
-
monitorenter:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
-
monitorexit:
- 执行monitorexit的线程必须是objectref所对应的monitor的所有者
- 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者
锁优化
- jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销
- 锁主要存在四中状态:
- 无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
- 他们会随着竞争的激烈而逐渐升级,但不可降级,目的是为了提高获得锁和释放锁的效率
自旋锁
- 自旋锁:让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。(怎么等待呢?执行一段无意义的循环即可)
- 原因:
- 线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作
- 在许多应用上面,,对象锁的锁状态只会持续很短一段时间
- 优点:可以避免线程切换带来的开销
- 缺点:占用了处理器的时间
- 在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;
自适应自旋锁
-
JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁
-
如何自适应:
-
自旋次数不固定,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
-
线程如果自旋成功了,那么下次自旋的次数会更加多;(上次成功这次还可能成功)
-
如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程
-
锁消除
- JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除
- 锁消除的依据是逃逸分析的数据支持
锁粗化
- 将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁
- 例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外
偏向锁
- 目的;为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,减少不必要的CAS操作,
- 实现方式:只需要检查是否为偏向锁(1)、锁标识位(01)以及ThreadID即可
- 获取锁:
- 检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
- 若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);
- 如果线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4)
- 通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
- 执行同步代码块
- 释放锁:
- 时机:
- 偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。
- 偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码);
- 操作:
- 暂停拥有偏向锁的线程,判断锁对象石是否还处于被锁定状态
- 撤销偏向锁,恢复到无锁状态(01)或者轻量级锁的状态
- 时机:
轻量级锁
- 目的:没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
- 实现方式:依赖多次CAS原子指令的;
- 注:对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢;
- 加锁时机:当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁
- 获取锁:
- 判断当前对象是否处于无锁状态(hashcode、0、01),若是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤(3)
- JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指正,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤(3)
- 判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态;
- 释放锁:
- 取出在获取轻量级锁保存在Displaced Mark Word中的数据;
- 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,否则执行(3)
- 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程。
REF
《深入理解java虚拟机》
《JAVA并发编程的艺术》
https://www.javazhiyin.com/15608.html
https://www.jianshu.com/p/e62fa839aa41