JAVA并发编程——Synchronized与锁升级

1.Synchronized的性能变化
2.synchronized锁种类及升级步骤
3.JIT编译器对锁的优化
4.总结
1.Synchronized的性能变化
我们都知道synchronized关键字能够让程序串行化执行,保证数据的安全性,但是性能会下降。
所以java对synchronized进行了一系列的优化:
java5之前:
synchronized仅仅只是synchronized,这个操作是重量级别的操作,cpu在进入加锁的程序后,会进行用户态和内核态之间的切换。
JAVA并发编程——Synchronized与锁升级
文章图片

用户态:用户态运行用户程序,级别较低
内核态:内核态运行操作系统程序,操作硬件,级别较高。
java如果要阻塞或者唤醒一个线程需要操作系统的接入,需要在用户态和核心态之间切换,因为synchronized属于重量级锁,是需要依赖底层操作系统的Mutex Lock来实现的,挂起线程和恢复线程都需要进入内核态去完成,这种切换会消耗大量的系统资源,如果同步代码块中的内容过于简单,这种切换的时间可能比用户代码的执行时间还长,时间成本太高,这也是为什么早起synchronized效率低的原因。
java6开始:
优化Synchronized,为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁,轻量级锁和重量级锁(减少了线程的阻塞和唤醒)。
2.synchronized锁种类及升级步骤
在说synchronized锁升级之前,我们要先搞清楚,线程访问一个synchronized修饰的方法,有三种类型:
1)只有一个线程来访问,有且唯一。
2)有两个线程A,B来交替访问
3)竞争激烈,多个线程来访问
还记得我们上一篇博客 JAVA并发编程——Java对象内存布局和对象头中提到了对象头,我们先来看看这张图:
JAVA并发编程——Synchronized与锁升级
文章图片

我们可以看出synchronized用的锁是存在java对象头的Mark Word中,锁升级功能主要依赖Mark Word中锁标志位和释放偏向锁标志位。
java的锁升级按照
无锁->偏向锁->轻量级锁->重量级锁
我们挨个进行讲解。
无锁:
我们先看一段无锁的代码

public static void main(String[] args) {//-XX:-UseCompressedClassPointers -XX:BiasedLockingStartupDelay=0 Object o = new Object(); System.out.println(ClassLayout.parseInstance(o).toPrintable()); }

这个对象没有使用锁,我们看一下运行的结果。
JAVA并发编程——Synchronized与锁升级
文章图片

这个输出的结果,对象头是倒着输出的,标红的地方便是锁标志位,现在是001,对应的对象头的图就是无锁状态。
偏向锁:
当一段同步代码一直被同一个线程多次访问,由于只有一个线程,那么该线程在后续访问的时候,就会自动获得锁。(这个锁偏向了经常访问的线程。)
在测试偏向锁之前,记得先输入jvm参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 因为偏向锁默认在jdk1.6之后默认是开启的,但是启动时间有延迟,所以需要手动添加参数,让偏向锁的延时时间为0,在程序启动时立刻开启。
Object o = new Object(); new Thread(() -> { synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); } },"t1").start();

运行结果为:
JAVA并发编程——Synchronized与锁升级
文章图片

轻量级锁:
刚刚上面讲述的只是有一个线程正在争抢一个资源类,但是现在有另外线程来逐步竞争锁的时候,就不能使用偏向锁了,要升级为轻量级锁。
在第一个线程正在执行synchronized方法(处于同步块),当它还没有执行完的时候,其它线程来抢夺,竞争线程使用cas更新对象头失败,该偏向锁会被取消并出现锁升级。
此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获取该轻量级锁。
//关闭延时参数,启用该功能 Object o = new Object(); new Thread(() -> { synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); } },"t1").start();

JAVA并发编程——Synchronized与锁升级
文章图片

那么竞争到底自旋多少次会进行锁升级呢?
java6之前:
默认情况下自旋的次数为10次:-XX:PreBlockSpin=10
或者自旋次数超过cpu核数一半
java6之后:
自适应:意味着自旋次数是不固定的。
是根据:同一个锁上次自旋的时间和拥有锁线程的状态来确定。
偏向锁和自旋锁的区别:
争夺轻量级锁失败的时候,自旋尝试抢占锁
轻量级锁每次退出同步代码块都需要释放锁,而偏向锁是在竞争发生时才释放锁。
重锁:
有大量线程正在抢占同一个资源类,冲突性很高,会升级成重量级锁。
new Thread(() -> { synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); } },"t1").start(); new Thread(() -> { synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); } },"t2").start(); new Thread(() -> { synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); } },"t3").start();

JAVA并发编程——Synchronized与锁升级
文章图片

3.JIT编译器对锁的优化
1)锁消除
当只有一个线程运行synchronized代码的时候,默认会把锁消除,节省资源。
/** * 锁消除 * 从JIT角度看相当于无视它,synchronized (o)不存在了,这个锁对象并没有被共用扩散到其它线程使用, * 极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用 */ public class LockClearUPDemo { static Object objectLock = new Object(); //正常的public void m1() { //锁消除,JIT会无视它,synchronized(对象锁)不存在了。不正常的 Object o = new Object(); synchronized (o) { System.out.println("-----hello LockClearUPDemo"+"\t"+o.hashCode()+"\t"+objectLock.hashCode()); } }public static void main(String[] args) { LockClearUPDemo demo = new LockClearUPDemo(); for (int i = 1; i 10; i++) { new Thread(() -> { demo.m1(); },String.valueOf(i)).start(); } } }

2)锁粗化
加入一个锁在同一个方法中,头尾相接,前后相邻的都是一个锁对象,那么编译器就会把这几个synchronized合并成一大块,加粗了范围,节省了资源。
/** * 锁粗化 * 假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块, * 加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能 */ public class LockBigDemo { static Object objectLock = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (objectLock) { System.out.println("11111"); } synchronized (objectLock) { System.out.println("22222"); } synchronized (objectLock) { System.out.println("33333"); } },"a").start(); new Thread(() -> { synchronized (objectLock) { System.out.println("44444"); } synchronized (objectLock) { System.out.println("55555"); } synchronized (objectLock) { System.out.println("66666"); } },"b").start(); } }

4.总结
锁升级的流程图片
JAVA并发编程——Synchronized与锁升级
文章图片

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法只有纳秒的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 只有一个线程访问同步块的场景
轻量级锁 竞争的线程不会阻塞,提高了线程的相应速度 始终得不到cpu的话,会空转,浪费cpu 追求相应时间,同步块执行速度非常快
重量级锁 线程竞争不适用自旋,不消耗cpu 线程阻塞,造成用户态和内核态的切换,响应时间慢 追求数据一致性,同步执行块执行速度较长
锁升级用一句话概括:
先自旋,不行再阻塞。
就是把之前的悲观锁(重量级锁)在变成一定条件下使用偏向锁以及使用轻量级锁。
【JAVA并发编程——Synchronized与锁升级】synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。
JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是无论什么情况都使用重量级锁。

    推荐阅读