4、Synchronized锁升级
在JDK1.6之前Synchronized只有重量级锁,没有获得锁的线程会阻塞,直到被唤醒才能再次获得锁,JDK1.6之后对锁做了很多优化引入了偏向锁、轻量级锁、重量级锁
4.1、无锁
public class Student { public static void main(String[] args) { Student stu=new Student(); System.out.println("10机制hashCode:"+stu.hashCode()); System.out.println("16机制hashCode:"+Integer.toHexString(stu.hashCode())); System.out.println("2机制hashCode:"+Integer.toBinaryString(stu.hashCode())); System.out.println(ClassLayout.parseInstance(stu).toPrintable()); } }
两行Value里面存储了8个字节的Mark Word,相当于如下两行数据
01 f2 b2 8d (00000001 11110010 10110010 10001101) (-1917652479) 56 00 00 00 (01010110 00000000 00000000 00000000) (86)
这里实际上包含了同样结果的两种数据格式二进制和16进制,去掉括号外面的数据,两行整合成一行
二进制
(00000001 11110010 10110010 10001101) (01010110 00000000 00000000 00000000)
16进制
01 f2 b2 8d 56 00 00 00
因为是小端存储,所以需要倒过来观看,数据顺序应该反过来
二进制
(00000000 00000000 00000000 01010110) (10001101 10110010 11110010 00000001)
16进制
00 00 00 56 8d b2 f2 01
56 8d b2 f2就是代表16机制hashCode
这样才是一个方便阅读的Mark Word结构,根据64位虚拟机的Mark Word结构示意图
最后三位为【001】,0代表偏向锁标记为,01表示锁标记
发现hashCode部分刚好等于打印出来的二进制HashCode:1010110 10001101 10110010 11110010,HashCode之所以不为空是因为调用了HashCode方法才显示出来
4.2、无锁升级偏向锁
public static void main(String[] args) { Student stu=new Student(); System.out.println("=====加锁之前======"); System.out.println(ClassLayout.parseInstance(stu).toPrintable()); synchronized (stu){ System.out.println("=====加锁之后======"); System.out.println(ClassLayout.parseInstance(stu).toPrintable()); } }
按照上诉步骤找到锁标记
最后三位为【000】,其中最后两位是【00】,按照之前的存储状态定义这是轻量级锁,本身没有存在锁竞争很明显不对,原因是因为JVM开启了偏向锁延迟加载,我们启动程序的时候偏向锁还没开启,在程序启动时添加参数:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 //关闭偏向锁的延迟
再次查看程序运行结果
加锁之后锁标记是【101】表示偏向锁是符合预期的,但发现没加锁之前锁也就是偏向锁,本应该是无锁
发现没加锁之前没有线程ID,加锁之后才有线程ID, thread指针 和 epoch 都是0,说明当前并没有线程获得锁,表示可偏向的状态,所以无锁也是一个特殊的偏向锁,当有线程获取到时才会真正变为偏向锁
偏向锁的主要作用就是当同步代码块被一个线程多次访问,只有第一次访问的时候需要记录线程的ID,后续就会一直持有着锁而不需要再次加锁释放锁,因为只有一个线程那么该线程在后续多次访问就会自动获得锁,为了提高一个线程执行的性能,而不需要每次都去修改对象头的线程ID还有锁标志才能够获得锁
4.3、偏向锁流程
偏向锁获取流程:
- 首先查看Mark Word中的锁标记以及线程ID是否为空,如果锁标记是101代表是可偏向状态
- 如果是可偏向状态,再查看线程ID是当前的线程,直接执行同步代码块
- 如果是可偏向状态但是线程ID为空或者线程ID已被其他线程持有,那么就需要通过CAS操作去修改Mark Word中线程ID为当前线程还有锁标记,然后执行同步代码块
- CAS修改失败的话,就会开始撤销偏向锁,撤销偏向锁需要达到全局安全点,然后检查线程的状态
- 如果线程还存活检查线程是否在执行同步代码块中的代码,如果是升级为轻量级锁进行CAS竞争
- 如果没有线程存活,直接把偏向锁撤销到无锁状态,然后另一个线程会升级到轻量级锁
偏向锁撤销:
一种出现竞争出现才释放锁的机制,另外有线程来竞争锁,不能再使用偏向锁了,需要升级为轻量级锁,原来的偏向锁需要撤销,就会出现两种情况:
- 线程还没有执行完,其他线程就来竞争,导致需要撤销偏向锁,此时当前线程升级为持有轻量级锁,继续执行代码
- 线程执行完毕退出了同步代码块,将对象头设置为无锁并且撤销偏向锁重新偏向
偏向锁批量重偏向:
当一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,会导偏向锁重偏向的操作,过程比较耗时,所以当撤销次数达到20次以上的时候,20这个值可以修改,会触发重偏向,直接把偏向锁偏向线程2
偏向锁就是一段时间内,只由一个线程来获得和释放锁,加锁的方式就是通过把线程ID保存到锁对象的Mark Word中
4.4、偏向锁升级轻量级锁
public class Student { public static void main(String[] args) { Student stu=new Student(); System.out.println("=====加锁之前======"); System.out.println(ClassLayout.parseInstance(stu).toPrintable()); synchronized (stu){ System.out.println("=====加锁之后======"); System.out.println(ClassLayout.parseInstance(stu).toPrintable()); } Thread thread=new Thread(){ @Override public void run() { synchronized (stu){ System.out.println("====轻量级锁===="); System.out.println(ClassLayout.parseInstance(stu).toPrintable()); } } }; thread.start(); } }
很明显由特殊状态的无锁->偏向锁->轻量级锁
偏向锁在不影响性能的情况下获得了锁,这时候如果还有一个线程来获取锁,如果没有抢占到就会自旋一定的次数,这个次数可以通过JVM参数控制,抢占到了锁就不需要阻塞,轻量级锁也称为自旋锁
这个自旋也是有代价的,如果线程数过多,一直都在使用自旋抢占线程会浪费CPU性能,所以自旋的次数必须要有个限制,JDK1.6中默认是10次,JDK1.6之后使用的自适应自旋锁,意味着自旋的次数并不是固定的,而是根据同一个锁上次自旋的时间,如果很少自旋成功,那么下次会减少自旋的次数甚至不自旋,如果自旋成功,会认为下次也可以自旋成功,会增加自旋的次数
4.5、轻量级锁流程
轻量级锁获取流程:
- 一个线程进入同步代码块,JVM会给每一个线程分配一个Lock Record,官方称之为“Dispalced Mark Word”,用于存储锁对象的Mark Word,可以理解为缓存一样存储了锁对象
- 复制锁对象的Mark Word到Lock Record中去
- 使用CAS将锁对象的Mark Word替换为指向Lock Record的指针,如果成功表示轻量级锁占锁成功,执行同步代码块
- 如果CAS失败,说明当前lock锁对象已经被占领,当前线程就会使用自旋来获取锁
轻量级锁释放:
- 会把Dispalced Mark Word存储锁对象的Mark Word替换到锁对象的Mark Work中,会使用CAS完成这一步操作
- 如果CAS成功,轻量级锁释放完成
- 如果CAS失败,说明释放锁的时候发生了竞争触发锁膨胀,膨胀完之后调用重量级的释放锁方法
轻量级锁加锁的原理就是,JVM会为每一个线程分配一个栈帧用于存储锁的空间,里面有个Lock Record数据结构,也就是BaseObjectLock对象,会把锁对象里面的Mark Word复制到自己的BaseObjectLock对象里面,然后使用CAS把对象的Mark Word更新为指向Lock Record的指针,如果成功就获取锁,如果失败表示已经有其他线程获取到了锁,然后继续使用自旋来获取锁
轻量级锁每次都需要释放锁,而偏向锁只有存在竞争的时候才释放锁为了避免反复切换
4.6、轻量级锁升级重量级锁
package com.ylc; import org.openjdk.jol.info.ClassLayout; public class Student { public static void main(String[] args) { Student stu=new Student(); System.out.println("=====加锁之前======"); System.out.println(ClassLayout.parseInstance(stu).toPrintable()); synchronized (stu){ System.out.println("=====加锁之后======"); System.out.println(ClassLayout.parseInstance(stu).toPrintable()); } Thread thread=new Thread(){ @Override public void run() { synchronized (stu){ System.out.println("====轻量级锁===="); System.out.println(ClassLayout.parseInstance(stu).toPrintable()); } } }; thread.start(); for (int i=0;i<3;i++){ new Thread(()->{ synchronized (stu){ System.out.println("====重量级锁===="); System.out.println(ClassLayout.parseInstance(stu).toPrintable()); } }).start(); } } }
锁的标志为【010】代表重量级锁
倘若通过自旋重试一定次数还获取不到锁,那么就只能阻塞等待线程唤醒了,最后升级为重量级锁
4.7、重量级锁流程
重量级锁获取流程:
- 首先会进行锁膨胀
- 然后会创建一个ObjectMonitor对象,通过该把该对象的指针保存到锁对象里面
- 如果获取锁失败或者对象本身就处于锁定状态,会进入阻塞状态,等待CPU唤醒线程重新竞争锁
- 如果对象无锁就会获取锁
重量级锁释放流程:
- 会把ObjectMonitor中的的持有锁对象owner置为null
- 然后从阻塞队列里面唤醒一个线程
- 唤醒的线程重新竞争锁,如果没有抢占到继续等待
由此可以发现Synchronized底层的锁机制是通过JVM层面根据线程竞争情况来实现的
5、Synchronized锁消除
Java虚拟机在JIT编译时会去除没有竞争的锁,消除没有必要的锁,可以节省锁的请求时间
public class Student { public static void main(String[] args) { for (int i=0;i<=10;i++){ new Thread(()->{ Student.lock(); }).start(); } } public static void lock(){ Object o=new Object(); synchronized (o){ System.out.println("hashCode:"+o.hashCode()); } } }
每次都加了锁,可是都不是同一把锁,无法产生竞争这样的锁没有意义,相当于会无视synchronized (o)的存在
6、Synchronized锁粗化
锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次操作。将多个联系的锁扩展为一个范围更大的锁
public class Student { public static void main(String[] args) { Object o=new Object(); new Thread(()->{ synchronized (o){ System.out.println("加一次锁"); } synchronized (o){ System.out.println("加两次锁"); } synchronized (o){ System.out.println("加三次锁"); } synchronized (o){ System.out.println("加四次锁"); } }).start(); } }
把小锁范围扩大,优化后变成
public class Student { public static void main(String[] args) { Object o=new Object(); new Thread(()->{ synchronized (o){ System.out.println("加一次锁"); } }).start(); } }
7、锁的优缺点对比
锁 | 优点 | 缺点 | 适用场景 |
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法仅有纳米级的差距 | 如果线程间存在锁的竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问的同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的相应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间 同步响应非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量 同步块执行速度较长 |