美文网首页
锁 - 偏向锁、轻量级锁、重量级锁及锁优化

锁 - 偏向锁、轻量级锁、重量级锁及锁优化

作者: 面向对象架构 | 来源:发表于2022-11-28 00:50 被阅读0次

偏向锁 轻量级锁 重量级锁

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。

随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。锁的状态是通过对象监视器在对象头中的字段来表明的。JDK 1.6中默认是开启偏向锁和轻量级锁的。

偏向锁

偏向锁是JDK6时加入的一种锁优化机制: 在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。偏是指偏心,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁及对Mark Word的更新操作等)。

如果需要,使用参数-XX:-UseBiasedLocking禁止偏向锁优化(默认打开)

优点: 把整个同步都消除掉,连CAS操作都不去做了,优于轻量级锁。

缺点: 如果程序中大多数的锁都总是被多个不同的线程访问,那偏向锁就是多余的。

轻量级锁

轻量级锁是JDK6时加入的一种锁优化机制。当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能

“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量。轻量级是相对于使用操作系统互斥量来实现的重量级锁而言的。但是,轻量级锁并不代替重量级锁的,其本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果出现两条以上的线程争用同一个锁的情况,那轻量级锁将不会有效,必须膨胀为重量级锁。

优点: 如果没有竞争,通过CAS操作成功避免了使用互斥量的开销。

缺点: 如果存在竞争,除了互斥量本身的开销外,还额外产生了CAS操作的开销,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。

重量级锁

典型的重量级锁:synchronized

当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

重量级锁是通过monitor实现的,锁的标志位为10,对象头中的指针指向monitor对象的起始地址。在java虚拟机中,monitor是由ObjectMonitor实现。位于\openjdk-jdk8u-jdk8u\openjdk-jdk8u-jdk8u\hotspot\src\share\vm\runtime包中。

Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。

为了优化synchonized,引入了轻量级锁,偏向锁。

锁消除

虚拟机在运行时,虽然代码进行了同步,但是如果虚拟机检测到不存在数据竞争时,虚拟机就会自动把锁进行消除。

锁消除主要的判定依据是如果堆上的所有数据都不会被其它的线程访问到,那么就可以理解为这些数据是线程私有的。既然是线程私有的,那么同步加锁也就没有存在的必要了,于是虚拟机就会将锁消除掉。

public class LockClearTest {
 
    public static void main(String[] args) {
        LockClearTest test = new LockClearTest();
 
        for (int i = 0; i < 100000; i++) {
            test.append("aaa", "bbb");
        }
    }
 
    public void append(String str1, String str2) {
   
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(str1).append(str2);
    }

}

    // StringBuffer的append代码如下
     @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

StringBuffer的append是个同步方法,但是LockClearTest中的 StringBuffer 属于一个局部变量,不可能从该方法中逃逸出去(即stringBuffer的引用没有传递到该方法外,不会被其他线程引用),因此其实这过程是线程安全的,可以将锁消除。

锁粗化

假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。

public class StringBufferTest {
 
    StringBuffer stringBuffer = new StringBuffer();
 
    public void append(){
        stringBuffer.append("a").append("b").append("c");
    }
}

上述代码每次调用 stringBuffer.append 方法都需要加锁和解锁,如果JVM检测到有一连串的对同一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

锁升级(锁膨胀)

锁膨胀也称为锁升级,并不同于锁粗化。锁粗化是指锁同步范围的扩大,而锁膨胀是指锁类型升级为更加重量级的锁。

Java中的锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁、重量级锁。这几个状态会随着竞争情况逐渐升级,锁可以升级但不能降级,意味着偏向锁升级为轻量级锁后不能降级为偏向锁。这种锁升级却不能降级的策略,目的是提高获得锁和释放锁的效率

锁升级过程可以简单理解为,每个对象起始状态都是最低级,但随着线程竞争的发生,这个对象的锁等级会一步一步提高,最终到达重量级锁。因为JDK6以后默认开启偏向锁,所以可以认为每个对象的初始状态都是偏向锁。

优点 缺点 适用场景
偏向锁 没有加锁、解锁的额外消耗,几乎没有不影响运行速度 如果线程间存在竞争,则会带来额外的锁撤销的消耗 只有一个线程访问同步块
轻量级锁 竞争的线程不会阻塞,提高响应速度 如果线程长时间得不到锁会一直自旋消耗CPU资源 同步块执行时间短,追求响应时间
重量级锁 线程竞争不会自旋,不额外消耗CPU 线程阻塞,响应缓慢 同步块执行时间长,追求吞吐量
  • 锁的升级过程:
    一旦对象锁升级为重级锁后,后续线程对该对象的操作都将是加锁为重量级锁
  • 锁不存在降级:
    锁只有从偏向锁->轻量级/重量级 或者 直接 轻量级-> 重量级. 不会降级,只有解锁

相关文章

网友评论

      本文标题:锁 - 偏向锁、轻量级锁、重量级锁及锁优化

      本文链接:https://www.haomeiwen.com/subject/kfzsxdtx.html