一. 基础复习
- synchronized,与 ReentrantLock 都属于悲观锁
- 乐观锁,例如Version,Atomic包下的原子类AtomicInteger 等,基于cas实现乐观锁
- synchronized 根据编写方式不同分为: 同步代码块,同步方法
- synchronized 可以修饰方法,修饰代码块,修饰内部类,修饰对象等
-
修饰非静态成员: 例如修饰this代码块,非静态方法等使用的是this锁,调用进入同步区域时就必须先获得对象锁。如果此对象的对象锁已被其他调用者占用,则需要等待此锁被释放,java的所有对象都含有一个互斥锁,这个锁由jvm自动获取和释放.synchronized方法正常返回或者抛异常而终止,jvm会自动释放对象锁。这里也体现了用synchronized来加锁的一个好处,即 :方法抛异常的时候,锁仍然可以由jvm来自动释放
-
修饰静态成员: 例如修饰class,静态代码块,静态方法等不论被实例化多少次,其中的静态方法和静态变量在内存中都只有一份。所以,一旦一个静态的方法被声明为synchronized。此类所有的实例对象在调用此方法,共用同一把锁,我们称之为类锁, 对象锁是用来控制实例方法之间的同步,而类锁是用来控制静态方法(或者静态变量互斥体)之间的同步的。类锁只是一个概念上的东西,并不是真实存在的,他只是用来帮助我们理解锁定实例方法和静态方法的区别的。java类可能会有很多对象,但是只有一个Class(字节码)对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,只不过是Class对象的锁而已。获取类的Class对象的方法有好几种,最简单的是[类名.class]的方式
二. 根据synchronized修饰不同成员了解synchronized实现原理
- 了解synchronized底层需要对使用锁的类进行反编译,反编译命令
- javap -c 需要反编译的类名.class
- 如果需要查看更多信息使用" javap -v XXX.class" 会输出包括行号,本地变量表等详细信息
同步代码块
- 示例代码,该示例中在method1方法中,使用synchronized修饰对象
public class SynchronizedTest {
private Object objLock = new Object();
public void method1(){
synchronized (objLock) {
System.out.println("执行");
}
}
public static void main(String[] args) {
}
}
- 执行"javap -c SynchronizedTest.class" 命令对上述代码生成的类进行反编译: 其中"monitorenter"表示加锁, “monitorexit” 表示释放锁,分析下图会发现,在执行执行method1()方法是会加锁,方法执行完毕后会释放锁,但是一个monitorenter加锁会出现两个monitorexit释放锁
- 示例代码二: method1()方法中手动抛出了一个异常,再次反编译查看
小总结
- synchronized代码块时: 底层在对象头的"mark_word"中设置"monitorenter"指令表示加锁,"monitorexit"指令表示释放锁
- monitorenter与monitorexit 是成对出现的吗: 通常情况下一个monitorenter加锁命令会对应两个monitorexit释放锁指令,原因是防止代码发生异常时锁无法释放问题(例如上方示例中演示)
- monitorenter与monitorexit 不是成对出现的那么monitorexit 一定会是两个吗?: 不一定,假设示例代码的method1()方法中我们手动抛出异常,再去对该class进行反编译,就会发现只出现了一个monitorenter对应一个monitorexit,并且会伴随两个athrow
同步方法
- 示例代码,method()普通方法被 synchronized 修饰
public class SynchronizedTest {
private Object objLock = new Object();
public synchronized void method() {
System.out.println("执行");
}
public static synchronized void method2() {
System.out.println("静态同步方法");
}
public static void main(String[] args) {
}
}
- 使用"javap -v XXX.class" 对该类进行反编译,查看mothod()普通同步方法发现普通同步方法时没有"monitorenter"与"monitorexit"锁相关的指令,而是通过在方法上flags加了一个"ACC_SYNCHRONIZED"标识
- 使用"javap -v XXX.class" 对该类进行反编译,查看mothod2()静态同步方法发现与普通同步方法比只多了一个"ACC_STATIC"标识
小总结
使用synchronized修饰方法实现锁原理: 与同步代码块不同,在执行调用指令时通过判断调用的方法是否被"ACC_SYNCHRONIZED"标识,如果有,执行线程会先持有monitor,然后再执行方法,方法执行完成释放monitor(无论方法正常或是异常都会释放),并且同步静态方法也是该方式与非同步方法比只是多了一个"ACC_STATIC"静态标识
synchronized 与 管程
- 管程: 进程与线程之间共享资源,但是要防止冲突,例如数据安全问题,管程就相当于这个协调者,Monitor(监视器),也可以说是我们平时说的锁,Monitor是一种同步机制,保证同一时间内只有一个线程访问某个数据和代码,举例: JVM中基于进入和退出监视器对象来实现的synchronized, java 虚拟机支持方法级同步,也就是上面的同步方法,与内部一段指令序列的同步也就是上面的同步代码块
- 根据上面同步代码块与同步方法反编译查看了解到:
- 同步一段指令序列(同步代码块): 在对象头的"mark_word"中设置"monitorenter"指令表示加锁,"monitorexit"指令表示释放锁,正常情况下一个monitorenter对应两个monitorexit,防止执行的代码块中发生异常锁无法释放,当代码块中手动抛出异常时,一个monitorexit对应一个monitorexit, 将异常释放放到了在异常抛到同步方法的边界之外时自动释放
- 方法级同步(同步方法): 是隐式同步,无序通过字节指令来控制,虚拟机通过判断方法常量池中该方法的表结构中是否有"ACC_SYNCHRONIZED"标志来判断这个方法是否是同步方法,如果是执行线程会先成功持有管程,然后才去执行方法,在执行期间其它线程无法获取同一个管程,如果该方法执行中抛出异常,那这个方法持有的管程将在异常抛到同步方法的边界之外时自动释放
底层分析
- java中所有类都继承自Object,在HotSpot虚拟机中,monitor管程是由ObjectMonitor实现继续往下扒(.cpp, .hpp是c中的)
- 了解 ObjectMonitor结构
//结构体如下
ObjectMonitor::ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; //线程的重入次数
_object = NULL;
_owner = NULL; //标识拥有该monitor的线程
_WaitSet = NULL; //等待线程组成的双向循环链表,_WaitSet是第一个节点
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多线程竞争锁进入时的单向链表
FreeNext = NULL ;
_EntryList = NULL ; //_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
- 其中几个关键属性
- 了解了java中所有类都继承自Object,每个object的对象里 markOop->monitor() 里可以保存ObjectMonitor的对象,这个ObjectMonitor就与ObjectMonitor结构,就解释了为什么每个对象就是一个监视器,就是一把锁
三. 锁升级相关
- 先查看上面对象头相关笔记,了解一下对象头,synchronized利用对象头中的MarkWord,根据锁标志位偏向锁标志位的不同实现锁升级策略
- 先提出synchronized锁升级过程是: 无锁 —> 偏向锁 —> 轻量级锁 —> 重量级锁
- 那聊一下为什么会出现锁升级: java5以前如果使用synchronized上来就是重量级锁,虽然保证了数据安全,但是会造成性能下降,假设对一个数据加锁,而实际访问这个数据时不是不出现是很少会出现多线程并发访问的场景,会造一种情况,大部分的时间都是单个线程去访问一个被加了重量级锁的数据,进而出现了锁升级
- 说一下重量级锁为什么会影响性能: 查看java中启动一个线程start源码会发现底层调用的是,native方法start0,如果继续向下通过OpenJDK去查看的话会发现调用了Thread.c中的JVM_StartThread,再向下JVM_StartThread中实际调用的是操作系统os的开启一个线程命令"start_thread()“,总结就是说:java早期版本synchronized加锁时监视器monitor依赖与底层操作系统的MutexLock实现,挂起线程与恢复线程都需要转入内核态完成,而java的线程最终会映射到操作系统原生线程上,如果要阻塞或者唤醒一个线程操作系统要介入,需要用户态到内核态之间的切换(也就是ObjectMonitor中的”_owner"属性 持有的线程切换),比较耗费时间性能
- java6 为了减少获取锁跟释放锁带来的性能消耗,引入了轻量级锁和偏向锁
复习一下对象头
- 对象在内存中存储的布局分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
- 对象头中又分为mark_word 与 klass_word两部分,其中mark_word中存储了对象在runtime时用的信息: 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳、转发指针等等
- 项目中引入jol-core依赖,通过对象头查看锁详情
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
- 举例查看创建的Object对象o的对象头信息
import org.openjdk.jol.info.ClassLayout;
public class MyObject {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
- 有两个注意点:
- 锁是通过对象头中的"mark_word"实现,在查看对象头信息时值关注前两行"mark_word"部分即可
- 通过"ClassLayout.parseInstance(“对象”).toPrintable()"查看输出对象头信息VALUE下括号中的数子编码时,输出的数字编码是二进制的,要从后向前看
- VALUE 下括号中的是二进制数据,括号前面是对应括号中的十六进制数据
- 在前面我们学习对象头时,已经了解到创建对象后每个对象都有它对应的hashCode,为什么此处没有,因为hashCode是调用获取时才会生成,否则不会生成
无锁演示
- 创建一个Object对象不加锁查看对象头
import org.openjdk.jol.info.ClassLayout;
public class MyObject {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
- 输出入下图
偏向锁
- 一段加锁的同步代码一直被同一个线程多次访问,由于只有这一个线程访问,在后续再访问时便会自动获得锁
- 在我们测试运行多个线程同时并发访问同一个加锁的数据时,实际会发现会有某几个或一个拿到锁的频率比较高,而另外几个线程获取到锁执行的频率普遍较低,也就是说实际测试下来锁总是被第一个占用它的线程获取到,这个线程就是偏向线程
- 先总结偏向锁的优点: 偏向锁的操作不涉及用户态到内核态的切换,同一个线程时不需要多次释放与获取锁,不存在CAS修改MarkWord线程指针
- 偏向锁是怎么实现的: 一个synchronized方法被线程获取到锁时,锁对象会在对象头MarkWord中标记为偏向锁状态,并且会用54位存储线程指针作为偏向标识,若该线程再次访问该锁时,只需要判断MarkWord中通过这54位存储的线程指针与当前线程是否一致,无需进入Monitor去竞争
- 在锁被第一次获取时,JVM使用CAS操作把线程指针记录到MarkWord中,这个线程就可以看成偏向线程,并通过CAS操作修改对象头中偏向锁标志位,该偏向线程一直持有着锁,后续该线程进入和退出加锁执行代码时不再需要再次获取和释放锁,而是直接比较对象头中是否存储了指向当前线程的偏向锁,有两种情况
- 判断对象头中偏向锁偏向当前线程: 不需要再次尝试获取锁,直接进入同步代码,并且不需要加锁释放锁时通过CAS更新对象头,几乎没有性能消耗
- 判断对象头中不是当前线程ID: 说明发送了竞争,然后进行重偏向,或升级变为轻量级锁操作
-
注意点: 偏向锁只有在其它线程竞争获取偏向锁时,持有偏向锁的线程才会释放锁,偏向锁时线程是不会主动释放偏向锁的
演示
- 执行查看服务器偏向锁相关数据命令: “java -XX: +PrintFlagsIntitial |grep BiasedLock*” 会发现默认是开启偏向锁"UseBiasedLocking = true" 但是有4000毫秒的延迟,所以要修改
- 执行命令修改延时4000毫秒为0 (IDEA中 “Run—>Edit Configurations —> VM options 中输入”-XX:BiasedLockingStartupDelay=0")
- 测试代码(创建一个锁对象,开启一个线程,线程中synchronized以创建的对象为锁监视器,打印锁对象对象头信息)
public static void main(String[] args) {
Object o = new Object();
new Thread(() -> {
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}, "t5").start();
}
- 输出"101"偏向锁
偏向锁撤销
- 偏向锁时,即使同步代码执行完毕,线程是不会主动释放锁的,线程在获取锁时会判断MarkWord中存储的线程指针不是当前线程时,通过CAS自旋修改对象头线程指针为当前线程的,如果CAS修改失败说明锁已经被其它线程获取,这时将阻塞,撤销偏向锁,撤销需要等待到全局安全点,同时检查持有偏向锁的线程是否还在执行(两种情况)
- 情况一: 第一个线程正在执行synchronized方法(处于同步块)还未执行完毕,此时其它线程进来争夺线程,该偏向锁会被取消,升级为轻量级锁,轻量级锁由原持有偏向锁的线程持有,从安全点继续执行同步代码,后面进来的竞争线程会进入自旋等待升级为轻量级锁的线程执行完毕释放,获取到该锁
- 情况二: 第一个线程执行完毕,第二个线程进来判断对象头MarkWord中线程指针不是当前线程的,会将对象头设置成无锁状态并撤销偏向锁,然后进行重偏向
- (什么是全局安全点STW=stop the world 虚拟机相关有解释)
偏向锁与hashCode
- 前面我们了解对象头时知道对象头的MarkWord中存储了如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳、转发指针等信息,64位操作系统上这些数据占64bit
- 查看一下无锁状态与偏向锁状态的不同: 在无锁状态时前56bit中有25bit不使用,31bit存储对象的hashCode,如果不获取这个对象的hashCode,那么用来存储hashCode的31bit是空,当获取是这31bit中才会有对应的值,而使用偏向锁时通过前54bit存储当前线程的线程指针,占用了存储hashCode的位置
- 问题: 如果一个对象的hashCode() 方法已经被调用过一次之后,这个对象还能被设置偏向锁么?答案是不能。因为如果可以的化,那 Mark Word中 的 identity hash code 必然会被偏向线程Id给覆盖,这就会造成同一个对象前后两次调用 hashCode() 方法得到的结果不一致
- HotSpot VM 的锁实现机制是:
- 当一个对象已经调用默认 hashCode() 或者 System.identityHashCode(),即计算过 identity hashcode 后,它就无法进入偏向锁状态。这意味着,如果要在不发生争用的对象上进行同步,则最好覆盖默认hashCode()实现,否则JVM不会优化。
- 当一个对象当前正处于偏向锁状态,需要计算 identity hashCode 的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁;
- 轻量级锁的实现中,会通过线程栈帧的锁记录存储 Displaced Mark Word;
- 重量锁的实现中,ObjectMonitor 类里有字段可以记录非加锁状态下的 mark word,其中可以存储 identity hash code 的值。
轻量级锁
- 先举个例子: 假设AB两个线程执行, A线程先获取到锁,后续B线程执行判断MarkWord中的线程指针不是当前B线程的,此时B线程CAS尝试获取锁,有两种情况: B线程CAS成功将MarkWord中的线程指针修改为当前线程说明获取偏向锁成功,并重偏向与当前线程,情况二如果CAS修改MarkWord失败,则偏向锁升级为轻量级锁,并且由原持有偏向锁的线程持有,此时B线程就会进入自旋等待A线程执行完毕释放轻量级锁
- 轻量级锁主要适用在没有多线程竞争情况下,通过CAS自旋再阻塞减少重量级锁的使用带来的性能消耗,
- 当获取轻量级锁失败时,会自旋等待,当自旋次数与等待线程数达到限制时,锁升级为重量级锁
- 轻量级锁每次退出同步块时都需要释放锁,
轻量级锁等待线程数与自旋次数问题
- 轻量级锁问题点: 上面说到过假设两个线程竞争执行同步代码块,A线程还未执行完毕,此时B线程会在外面CAS等待,假如说A线程始终不释放锁,这时候B线程一直在外面CAS会销毁大量CPU资源,假如A线程不释放接着进来了B,C,D等多个线程呢,防止这个问题,轻量级锁设置了自旋的次数,与竞争线程数规定,当超过这个次数轻量级锁将不再自旋,升级为重量级锁
- java6前: 自旋线程数,自旋次数限制默认开启:默认自旋次数10次,或自旋线程数超过cpu核的一半会放弃自旋升级为重量级锁
- java6后: 底层根据同一个锁上一次自旋的时间,拥有锁线程的状态来决定的
- 通过"-XX:PreBlockSpin=10"修改
轻量级锁演示
- 执行命令"-XX:-UseBiasedLocking"关闭偏向锁
- 执行代码,由于关闭偏向锁,加锁时直接到轻量级锁
public static void main(String[] args) {
Object o = new Object();
new Thread(() -> {
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}, "t5").start();
}
JIT锁消除与锁粗化
- 锁消除, 先查看下方一段代码
//外部声明了一个锁对象
private static Object lock = new Object();
//而m1方法内部会new出来新的锁对象
//此时m1每执行一次都会新建一个锁对象
//synchronized锁的各自的新的对象,会出现锁不住的情况
public void m1(){
Object lock = new Object();
synchronized (lock) {
}
}
- 锁粗化,同一个线程短时间内多次获取同一把锁时,JIT会优化将多次获取锁合成一次获取
//声明锁对象
private static Object lock = new Object();
public void m1(){
//在t1线程中多次获取同一把锁lock
new Thread(()->{
synchronized (lock) {
System.out.println("11111");
}
synchronized (lock) {
System.out.println("2222");
}
synchronized (lock) {
System.out.println("3333");
}
synchronized (lock) {
System.out.println("4444");
}
synchronized (lock) {
System.out.println("5555");
}
},"t").start();
}
//优化后可看为是下方
public void m2() {
//在t1线程中多次获取同一把锁lock
new Thread(() -> {
synchronized (lock) {
System.out.println("11111");
System.out.println("2222");
System.out.println("3333");
System.out.println("4444");
System.out.println("5555");
}
}, "t").start();
}
小总结
- JDK1.6后对synchronized进行了优化,升级过程: 无锁—>偏向锁—>轻量级锁—>重量级锁
- 内部还伴随的锁的重偏向,批量撤销等
- 重量级锁为什么性能低:
- 轻量级锁存在的问题,轻量级锁升级为重量级锁的条件
- 不同锁直接的优缺点对比(实际就是访问同步代码线程数,是否出现竞争的问题)
四. synchronized 可重入分析
- synchronized是可重入锁,每个锁对象都有一个锁计数器,跟执行持有该锁线程的指针,当执行"monitorenter"加锁命令时,如果目标锁对象计数器为0,那么说明他没有被其它线程获取,java虚拟机会将该锁对象持有线程设置为当前线程,并对计数器进行加一,在目标锁对象计数器不为0时判断锁对象中线程是否是当前线程,如果是则计数器进行累加操作,否则阻塞等待释放,当执行monitorexit释放锁时,会对计数器进行累减操作,当计数器为0时表示释放锁
- 前面了解过ObjectMonitor结构体,也就是ObjectMonitor结构体中的: "_owner "属性表示指向持有当前ObjectMonitor对象的线程, “_recursions” 属性表示锁重入次数
五. 总结
synchronized 底层原理
- synchronized实现原理: 在说synchronized实现原理前首相需要了解对象头结构与管程
- 对象布局: 对象在堆内存中,存储布局可划分为三个部分:对象头Header, 实例数据Instance Data, 和对齐填充Padding,其中对象头内部是由对象标记MarkWord, 类型指针Class Pointer(又叫类元信息klass pointer) 两部分组成,对象标记MarkWord中用"31bit” 存储对象的哈希码,“4bit” 存储对象分代年龄,"1bit"用来标记是否偏向锁,再未获取对象hashCode时,有54bit存储偏向锁的线程id,等信息
- 管程: 在java中所有类都继承自Object,每个object的对象里 markOop->monitor() 里可以保存ObjectMonitor的对象,这个ObjectMonitor就是指管程monitor对象,在ObjectMonitor有几个需要关注的属性:
- _owner: 持有ObjectMonitor对象线程
- _WaitSet: 存放处于wait状态的线程队列
- _EntryList: 存放处于等等锁block状态的线程队列
- _recursions: 锁的重入次数
- _count: 记录该线程获取锁的次数
- 当多个线程同时访问一段同步代码时:
- 首先会进入_EntryList集合,当线程获取到对象的monitor后,进入_owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
- 若线程调用wait()方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入WaitSet集合中等待被唤醒;
- 当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);
- 同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。
监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问,锁状态是被记录在每个对象的对象头(Mark Word)中,下面我们一起认识认识一下对象的内存布局
- 使用javap -c 或javap -v 对加锁代码进行反编译,会发现synchronized同步代码块与同步方法底层实现原理不同
- synchronized同步代码块底层原理
- 底层在对象头的"mark_word"中设置"monitorenter"指令表示加锁,"monitorexit"指令表示释放锁
- 通常情况下一个monitorenter加锁命令会对应两个monitorexit释放锁指令,防止代码发生异常时锁无法释放
- monitorenter与monitorexit 不是成对出现的那么monitorexit 一定会是两个吗?: 不一定,假设示例代码的method1()方法中我们手动抛出异常,再去对该class进行反编译,就会发现只出现了一个monitorenter对应一个monitorexit,并且会伴随两个athrow
- synchronized同步方法底层原理
与同步代码块不同,在执行调用指令时通过判断调用的方法是否被"ACC_SYNCHRONIZED"标识,如果有,执行线程会先持有monitor,然后再执行方法,方法执行完成释放monitor(无论方法正常或是异常都会释放),并且同步静态方法也是该方式与非同步方法比只是多了一个"ACC_STATIC"静态标识
synchronized 锁升级原理
- 先聊一下为什么要升级优化: java5以前synchronized是重量级锁,加锁时监视器monitor依赖与底层操作系统的MutexLock实现,挂起线程与恢复线程都需要转入内核态完成,而java的线程最终会映射到操作系统原生线程上(查看启动线程start()源码会发现底层调用的是,native方法start0,继续向下追最终会执行到OpenJDK中的JVM_StartThread, 是操作系统os的开启一个线程命令"start_thread()“),如果要阻塞或者唤醒一个线程操作系统要介入,需要用户态到内核态之间的切换(也就是ObjectMonitor中的”_owner"属性 持有的线程切换),比较耗费时间性能
- 锁升级的过程: 无锁—> 偏向锁—> 轻量级锁—>重量级锁
- 偏向锁
- 偏向锁的优点: 偏向锁的操作不涉及用户态到内核态的切换,同一个线程时不需要多次释放与获取锁,不存在CAS修改MarkWord线程指针, 加锁线程执行完毕后,不会主动释放锁
- 获取偏向锁流程: 当线程执行被synchronized修饰的代码时,会在对象头MarkWord中标记为偏向锁状态,并使用54位存储线程指针作为偏向线程标识,当再次获取锁时首先判断线程id与MarkWord中存储的是否一致
- 一致: 不需要再次尝试获取锁,直接进入同步代码,并且不需要加锁释放锁时通过CAS更新对象头,几乎没有性能消耗
- 不一致: 说明发送了竞争,JVM进行CAS操作修改MarkWord存储的线程指针为当前线程进行重偏向,或执行偏向锁锁撤销
- 偏向锁锁撤销: 如果CAS修改失败说明锁已经被其它线程获取,这时将阻塞,撤销偏向锁,撤销需要等待到全局安全点,同时检查持有偏向锁的线程是否还在执行(两种情况)
- 锁被其它线程获取还未执行完毕,此时其它线程进来争夺线程,该偏向锁会被取消,升级为轻量级锁,轻量级锁由原持有偏向锁的线程持有,从安全点继续执行同步代码,后面进来的竞争线程会进入自旋等待升级为轻量级锁的线程执行完毕释放,获取到该锁
- 前面获取锁线程执行完毕,当前线程进来判断对象头MarkWord中线程指针不是当前线程的,会将对象头设置成无锁状态并撤销偏向锁,然后进行重偏向
- 偏向锁还有一个注意点: 偏向锁与hashCode, 偏向锁是由对象头中的MarkWord中存储偏向线程id来实现的,如果一个对象获取了hashCode,会造成对象头中无法存储偏向线程id,或获取偏向锁后,执行hashcode将对象头中记录的偏向线程id给覆盖了, HotSpot VM 规定
- 当一个对象执行 hashCode() 或System.identityHashCode(),计算了 hashcode 后,则无法进入偏向锁状态。这意味着,如果要在不发生争用的对象上进行同步,则最好覆盖默认hashCode()实现,否则JVM不会优化。
- 当一个对象当前正处于偏向锁状态,需要计算 identity hashCode 的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁;
- 轻量级锁的实现中,会通过线程栈帧的锁记录存储 Displaced Mark Word;
- 重量锁的实现中,ObjectMonitor 类里有字段可以记录非加锁状态下的 mark word,其中可以存储 identity hash code 的值。
- 返回头来再说一下为什么要有偏向锁:在我们测试多线程并发争抢锁时,由于CPU调度问题会发现有有几个线程获取锁的概率始终比较高,也就是会出现饥饿,根据这一特性同一个线程,直接进入同步代码没有性能消耗, 然后再说一下如何解决锁饥饿,例如JUC下的ReentrantLock公平锁
- 轻量级锁
- 先举个例子: 假设AB两个线程执行, A线程先获取到锁,后续B线程执行判断MarkWord中的线程指针不是当前B线程的,此时B线程CAS尝试获取锁,有两种情况: B线程CAS成功将MarkWord中的线程指针修改为当前线程说明获取偏向锁成功,并重偏向与当前线程,情况二如果CAS修改MarkWord失败,则偏向锁升级为轻量级锁,并且由原持有偏向锁的线程持有,此时B线程就会进入自旋等待A线程执行完毕释放轻量级锁
- 轻量级锁每次退出同步块时都需要释放锁,
- 重量级锁
- 上面说到过假设两个线程竞争执行同步代码块,A线程还未执行完毕,此时B线程会在外面CAS等待,假如说A线程始终不释放锁,这时候B线程一直在外面CAS会销毁大量CPU资源,假如A线程不释放接着进来了B,C,D等多个线程呢,防止这个问题,轻量级锁设置了自旋的次数,与竞争线程数规定,当超过这个次数轻量级锁将不再自旋,升级为重量级锁
- java6前: 自旋线程数,自旋次数限制默认开启:默认自旋次数10次,或自旋线程数超过cpu核的一半会放弃自旋升级为重量级锁, java6后: 底层根据同一个锁上一次自旋的时间,拥有锁线程的状态来决定的
- synchronized 可重入分析: synchronized是可重入锁,每个锁对象都有一个锁计数器,跟执行持有该锁线程的指针, 前面了解过ObjectMonitor结构体,也就是ObjectMonitor结构体中的: "_owner "属性表示指向持有当前ObjectMonitor对象的线程, “_recursions” 属性表示锁重入次数