前言
到这里,大家应该都知道,Java中都有哪些锁,做什么用的了;
那么,不知道大家有没有听过Java锁的膨胀机制?
synchronized
JDK1.6 前只有重量级锁,JDK1.6之后,jvm对其进行优化,增加了偏向锁和轻量级锁(这两种锁咱们在上一篇已经解释过了,就不在复述了);
对象头
了解锁膨胀之前,咱们需要先了解下对象头;
在JVM的实现中每一个对象都会有一个对象头,它是用于保存对象的系统信息,对象头中有一个官方称为 MarkDown 的部分,他就是实现各种锁的关键;
在32位系统里,MarkDown 是32位的数据,64位系统中是64位的数据;
存放了对象的哈希值、对象年龄、锁的指针等等信息,总之就是一个对象的锁是否被占用、占用的是哪种锁,就记录在MarkDown中;
synchronized 升级之一阶段(偏向锁)
当线程访问同步代码块并获取锁时,会在 Mark Word 里存储锁偏向的线程 ID;
在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁;
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的;
偏向锁的撤销:
- 在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态
- 线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态
JVM 参数:
关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态
synchronized 升级之二阶段(轻量级锁)
轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能;
升级过程:
- 线程在自己的栈桢中创建锁记录 LockRecord
- 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中
- 将锁记录中的Owner指针指向锁对象
- 将锁对象的对象头的MarkWord替换为指向锁记录的指针,并将对象 Mark Word 的锁标志位设置为“00”,表示此对象处于轻量级锁定状态
出现轻量级锁的两种情况:
- 当关闭偏向锁功能时
- 多个线程竞争偏向锁导致偏向锁升级为轻量级锁
轻量级锁: CAS 操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能;
轻量级锁再次膨胀情况:
- 若当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁
- 当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁
synchronized 升级之三阶段(重量级锁)
重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁;
当有一个线程获取重量级锁之后,其余所有等待获取该锁的线程都会处于阻塞状态,操作系统实现线程之间的切换需要从用户态切换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长,所以它的切换成本非常高,这同时也说明了为什么重量级线程开销很大
的原因;
总结
通过上面的分析,我们知道了 synchronized 锁的演变过程,这就告诉我们,假如一开始就知道某个同步代码块的竞争很激烈,那么一开始就应该使用重量级锁了,节省掉一些锁转换的开销。