前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >synchronized的偏向、轻量、重量级锁

synchronized的偏向、轻量、重量级锁

作者头像
青山师
发布2023-05-05 20:13:11
1910
发布2023-05-05 20:13:11
举报

synchronized的偏向、轻量、重量级锁

Synchronized实现同步的方式有三种:偏向锁、轻量级锁、重量级锁。本文会从理论和代码实践两方面阐述三种锁的实现细节和原理。

偏向锁

偏向锁的思想很简单,就是偏向于第一个获取锁的线程,当其他线程要获取锁时,会在CAS操作中失败,然后挂起等待,直到第一个线程释放锁。这个锁的好处是可以满足大多数同步场景下的需求,并且消耗很小的资源。

要开启偏向锁,需要添加JVM参数-XX:+UseBiasedLocking

代码语言:javascript
复制
public class BiasedLocking {
    private Object lock = new Object();

    public void method1() {
        synchronized (lock) {
            // do something...
        }
    }
} 

当第一个线程进入synchronized块时,会将锁的标记从none修改为bias状态,同时记录偏向的线程ID。之后其他线程要获取锁,会通过CAS操作尝试将锁偏向自己,但这个操作会失败,所以只会短暂地竞争,很快其他线程就会进入阻塞状态,释放CPU时间片。

当偏向的线程退出同步块时,如果发现锁还没有其他线程在等待,那么会将锁的状态重置为none。如果发现有其他线程在等待,会释放锁,让等待线程获取。

当偏向的线程退出同步块时,如果发现锁还没有其他线程在等待,那么会将锁的状态重置为none。如果发现有其他线程在等待,会释放锁,让等待线程获取。

轻量级锁

轻量级锁的获取过程是通过CAS操作完成的。当前线程会先在对象头中记录自己,然后尝试用CAS将对象头中的锁记录替换为当前线程,如果成功就获取到锁,失败就进入阻塞队列等待唤醒。

代码语言:javascript
复制
public class LightWeightLock {
    private Object lock = new Object();

    public void method1() {
        synchronized (lock) {
            // do something...
        }
    }
}

当第一个线程进入方法时,会将对象头中的锁状态修改为当前线程ID,然后进入同步块。其他线程要获取锁时,会先检查对象头的锁状态,如果发现锁已经被占用,那么会使用CAS操作进行抢占,如果成功则获取到锁,失败会加入到阻塞队列进行等待。

当前线程退出同步块时,会使用CAS操作释放锁,将对象头设置为unlocked状态,同时唤醒阻塞队列中的一个等待线程。

轻量级锁的优点是消耗资源小,对代码性能的影响小,但是在高并发的场景下,CAS操作的 ABA问题会导致线程无法正常工作,所以当锁重入超过10次,或者锁持有时间超过1s时,JVM会将轻量级锁升级为重量级锁。

重量级锁

重量级锁会导致当前拥有锁的线程和其他等待线程都进入阻塞状态,切换到内核态,这 obviously 是一个非常消耗资源的操作。

其实现过程是:当前线程首先会在对象头中记录自己,然后进入内核态被阻塞,同时其他线程也会被阻塞。当其中一个线程退出同步块时,会唤醒其他线程中的一个

代码语言:javascript
复制
public class HeavyWeightLock {
    private Object lock = new Object();

    public void method1() {
        synchronized (lock) {
            // do something...
        }
    }
}

当第一个线程进入同步块时,会标记对象头表示此对象处于锁定状态,然后进入内核态挂起。其他线程要获取锁时,会发现对象头的锁定状态,也会进入内核态挂起。

当锁定的线程退出同步块时,会标记对象头为解锁状态,然后唤醒一个等待线程。被唤醒的线程会重新标记对象头为锁定状态,然后继续执行同步块中的内容。

重量级锁的优点是可以解决轻量级锁中的ABA问题,但是其性能消耗也是最大的。所以如果一个锁仅被一个线程使用,或有很高的重入概率,那么应选择偏向锁或轻量级锁,可以获得更高的性能。

偏向锁适用于单线程环境,性能最高;轻量级锁通过CAS实现,性能较好,但是会出现ABA问题;

操作步骤

要实际观察Synchronized锁的三种状态转换,可以使用JDK自带的JMC(Java Mission Control)工具。

下面是具体的操作步骤:

  1. 启动JMC,打开“标记对象(Mark Objects)”功能。
  2. 设置需要追踪的对象,这里我们选择上面的Demo对象。
  3. 运行代码,多线程访问这个对象的同步方法。
  4. JMC中打开“标记对象(Mark Objects)”视图,可以观察到对象头的状态在变化:
    • 初始为none状态,表示无锁
    • 第一个线程进入同步块后变为biased状态,表示偏向锁定
    • 多线程访问后变为轻量级锁,对象头记录为线程ID
    • 重入超过10次或持有超过1s后,变为重量级锁,对象头记录为锁定状态
  5. 当线程退出同步块后,可以观察到锁的释放过程
    • 偏向锁会重置为none状态
    • 轻量级锁使用CAS设置为unlocked状态,并唤醒后继线程
    • 重量级锁设置对象头为unlocked状态,并唤醒后继线程
  6. 可以尝试提高并发量或设置不同的超时时间,观察偏向锁和轻量级锁什么情况下会升级为重量级锁

通过上述步骤,我们可以直观的观察Synchronized锁的三种状态之间的切换过程,这也是理解其原理的最佳途径。

运维实施

在实际项目中,我们如何根据场景选择和设置合适的锁机制呢?这里提供一些参考建议:

  1. 偏向锁:默认开启,适用于大多数轻 contention 的场景,可以通过-XX:-UseBiasedLocking关闭。
  2. 轻量级锁:默认开启,无需配置,在大部分场景下可以获得不错的性能,如果出现ABA问题,会自动升级到重量级锁。
  3. 重量级锁:无需配置,在以下场景会自动使用:
    • 偏向锁或轻量级锁升级
    • JVM发现轻量级锁CAS操作次数过高
    • 同步块内耗时较长(默认超过1s)
  4. 锁消除:可以通过-XX:+EliminateLocks开启,JVM会分析代码,消除不可能存在竞争的锁,以提高性能。
  5. 锁粗化:可以通过-XX:+DoEscapeAnalysis 开启,JVM会分析代码,将多个连续的加锁操作锁合并为一个,以减少加锁操作次数。
  6. 自旋锁:可以通过-XX:PreBlockSpin设置自旋次数,指令在获取锁失败先自旋一定次数,再进入阻塞状态,以减少线程切换。但是如果自旋太长,会消耗CPU,需要根据场景设置。
  7. 锁定超时:可以通过-XX:MonitorTimeout=x设置重量级锁定超时时间,以避免线程因锁定过长出现死锁现象。

除上述JVM层面的设置外,在代码层面我们也可以根据场景选择不同的锁来提高性能,比如使用ReentrantLock代替Synchronized等。

面试相关

理解Synchronized锁及其实现原理,是Java后端工程师的基础知识之一,这部分内容在面试中也常会涉及,下面是一些可能的面试题:

  1. Synchronized的作用是什么?它的实现原理是什么? 答:Synchronized关键字实现同步,使得运行在同一进程中的多个线程操作同步代码段(或方法)时是互斥的。它的实现方式有三种:偏向锁、轻量级锁、重量级锁。具体可以参考本文前述内容。
  2. 偏向锁、轻量级锁、重量级锁的优缺点分别是什么? 可以参考本文“总结”部分的内容。偏向锁资源消耗最少,单线程场景使用;轻量级锁性能较好,使用CAS实现,存在ABA问题;重量级锁性能最差但安全,用于阻塞线程和处理ABA问题。
  3. Synchronized如何进行锁升级? 当偏向锁被不同线程获取超过20次,或轻量级锁被不同线程获取超过10次、或持有时间超过1s,Synchronized会进行锁升级。升级规则如下:
    • 偏向锁升级为轻量级锁
    • 轻量级锁升级为重量级锁
    • 重量级锁不会再降级

    锁升级的目的是为了提高并发性能。偏向锁适用于单线程,升级为轻量级锁可以适应更高的并发;轻量级锁使用CAS有性能损耗,升级为重量级锁可以解决该问题。

  4. JDK1.6之前的Synchronized如何实现?现在的实现方式有何不同? 答:JDK1.6之前,Synchronized只有一种实现方式:重量级锁。 JDK1.6之后,引入了偏向锁和轻量级锁,实现方式更加灵活,可以根据场景选择,大大提高了并发性能。重量级锁会让线程进入阻塞状态,拥有锁的线程与其他线程都阻塞在内核态,资源消耗大。偏向锁和轻量级锁让线程可以在用户态取得锁,资源消耗小,性能更好。所以现代JDK的Synchronized实现方式相比以前更加智能化,可以根据实际场景选择合适的锁,以obtian更好的并发性能。
  5. 是否应该使用Synchronized优先于ReentrantLock?两者的区别是什么? 答:这两个都是可重入锁,用于实现同步功能。 Synchronized是JVM实现的,性能稍差但使用简单,自动释放锁,不会出现死锁风险。 ReentrantLock是JDK实现的,性能更好,可以设置公平锁、锁定超时时间等,但是使用不当会造成死锁,并且需要手动释放锁,否则可能导致资源泄漏。 所以是否应该优先使用,需要根据实际场景决定:
    • 简单同步场景,没有特殊要求,使用Synchronized简单可靠
    • 追求高性能或需要设置特殊锁策略,使用ReentrantLock
    • 资源竞争激烈,且同步块时间长,使用ReentrantLock并设置锁定超时时间可以避免死锁

    两者的主要区别如下:

    • Synchronized是隐式锁(内置语法),ReentrantLock是显式锁(API)
    • Synchronized无法设置锁定超时,ReentrantLock可以
    • Synchronized粗暴地让线程阻塞在内核态,ReentrantLock可以先自旋再阻塞
    • Synchronized适合少量同步代码段,ReentrantLock可以同时锁定多个同步资源
  6. 对锁的理解?锁主要有哪几种? 答:锁是用于实现同步的机制,保证共享资源被线程排他地访问。主要有以下几种锁:
    • 偏向锁:锁定一次后,后续的锁定由同一线程完成,适用于单线程或同一线程重复加锁的场景
    • 轻量级锁:使用CAS操作进行加锁,性能好但存在ABA问题,用于短期加锁
    • 重量级锁:进入内核态阻塞,其他线程也阻塞,性能差但安全,用于长期加锁
    • 自旋锁:获取锁失败后先自旋一定次数再阻塞,适用于锁定时间很短的场景,可以减少线程切换开销
    • 可重入锁:同一线程可以多次获取同一把锁,Synchronized和ReentrantLock都是可重入锁
    • 读写锁:读锁可以被多个线程同时获取,写锁是排他锁,在追求读写并发场景使用
    • 死锁:两个或两个以上线程分别占有一部分资源并等待其他资源,导致无限期等待,需要避免出现
本文参与?腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-05-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客?前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • synchronized的偏向、轻量、重量级锁
    • 偏向锁
      • 轻量级锁
        • 重量级锁
          • 操作步骤
            • 运维实施
              • 面试相关
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
              http://www.vxiaotou.com