锁消除、锁粗化、偏向锁、适应性锁

文章目录

    • 锁消除
    • 锁粗化
    • 偏向锁
    • 适应性锁

锁消除

锁消除是JIT编译器对内部锁实现的一种优化。JIT可以借助逃逸分析来判断同步块的锁对象是否只是被一个线程访问,如果是的话,则在编译期间不生成内部锁的申请与释放对应的机器码,即消除了锁的使用。这种技术被称为锁消除。它可以减少锁的开销。
在这里插入图片描述
上面的代码在编译时,经过锁消除后,可以等效为下面的:
在这里插入图片描述

StringBuffer是线程安全的类,其append方法和toString方法都是用内部锁进行修饰的,但是当作为方法的局部变量时,因为只会有当前线程对该变量进行访问,所以JIT会进行锁的消除。
如下面代码,当JIT编译doSomething方法时,会将stringBuffer.append和 stringBuffer.toString方法复制到doSomething方法的方法体中,这种技术被成为内联。下面的代码,会对StringBuffer的append和toString方法进行锁的消除。

  private static void doSomething() {

        StringBuffer stringBuffer = new StringBuffer();

        stringBuffer.append("1");

        String s = stringBuffer.toString();
    }

注意:
并不是所有的方法都会进行锁消除,锁的消除技术依赖于JIT的内联优化,至于哪些方法会被内联,要取决于该方法的热度和该方法字节码的尺寸。

锁消除的好处是我们在需要使用锁的场景时,不用考虑锁的开销,只需要关注我们的业务逻辑即可,JIT会对我们的代码进行锁的消除。
但并不是所有方法都会进行锁消除,这个还是要取决于JIT。锁消除是默认开启的。

锁粗化

锁粗化也是JIT编译器对内部锁的一种优化。
锁消除、锁粗化、偏向锁、适应性锁_第1张图片

如上面的代码,这些同步块使用的是同一把锁,则JIT会将这些同步块合并为一个大的同步块,从而减少了锁申请、释放的开销。上面的代码会被JIT粗化成如下代码:
锁消除、锁粗化、偏向锁、适应性锁_第2张图片
相邻同步块之间如果存在其他语句,也不一定会阻碍JIT编译器进行锁的粗化,因为JIT编译器可能会在执行锁粗化前进行指定重排序,将这些语句排序到同一临界区内。
锁粗化可能会使一个线程持有一个内部锁的时间变长,从而导致其他线程申请该锁的时间也变长,阻塞了其他线程。因此,锁粗化不会被应用到循环体内的相邻同步块中。锁粗化是默认开启的。

偏向锁

偏向锁是java虚拟机对锁实现的一种优化。当大多数锁没有被争用时,并且在整个生命周期至多只会被一个线程持有,java虚拟机也会对其进行申请锁和释放锁,这就带来了昂贵的开销。因此,jvm会为每个对象维护一个偏好,即一个对象对应的内部锁第一次被一个线程获得时,则该线程就会被记录为该对象的偏好线程,这个线程在后续对该锁的访问都无需申请和释放锁,从而减少了开销。

然而,当该对象被偏好线程以外的线程申请锁时,jvm会回收该对象对原线程的偏好,并重新设置该对象的偏好线程,这个过程的代价也比较昂贵,因此,偏向锁的优化只适用于当前对象没有被大量线程争用的场景。
偏向锁的优化是默认开启的。

适应性锁

适应性锁是JIT编译器对内部锁实现所做的一种优化。
当一个线程获取某个对象的锁时,该锁如果被其他线程占用,则该线程会进行暂停,变为非Runnable状态,由于暂停会导致上下文切换,因此暂停这种方法适用于大多数线程对该锁持有时间比较长的场景,这样才能抵消上下文切换的开销。另外一种方式就是采用忙等,忙等就是当锁被占有时,当前线程不被阻塞,而是执行空的循环,直到获取到锁。
忙等的好处是不会导致上下文切换,但如果等待的时间太长,则会一直执行下去,从而消耗cpu资源。因此忙等适用于绝大多数线程对该锁的持有时间较短的场景。

jvm可以根据不同的场景来选择上面的两种策略。对于线程持有时间较长的锁,jvm会采用暂停的策略,对于线程持有时间较短的锁,jvm会采用忙等的策略。jvm也可能先采用忙等的策略,忙等失败的情况下再采用暂停的策略。jvm的这种优化被成为适应性锁

你可能感兴趣的:(java,多线程,java,jvm,后端,面试)