Java锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁

说明

JDK1.6为了减少获得锁和释放锁所带来的性能消耗,引入了"偏向锁"和"轻量级锁",所以在JDK1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

Java 中锁有几种状态:无锁 → 偏向锁 → 轻量级锁 → 重量级锁

注意:在 JDK 15 的版本上面,移除了偏向锁了

锁的几种状态

图片是在百度上面找的

无锁状态

程序不会有锁的竞争。第一个线程执行完之后,第二个线程再去执行。

偏向锁

偏向锁,字面理解就是说会偏向于第一个访问锁的线程

  1. 当一个线程访问一个同步代码块时,首先会尝试获取偏向锁。
  2. 如果该对象没有被其他线程锁定,并且没有发生过竞争,那么当前线程会将对象头中的标记设置为偏向锁,并将线程ID记录在对象头中。
  3. 当其他线程尝试获取该对象的锁时,会检查对象头中的标记,如果是偏向锁并且线程ID与当前线程ID相同,表示可以获取锁,无需进行同步操作。
  4. 如果其他线程尝试获取该对象的锁时,发现对象头中的标记不是偏向锁或者线程ID不匹配,表示发生了竞争,偏向锁会升级为轻量级锁。

偏向锁的优势在于减少了同步操作的开销,适用于大部分情况下只有一个线程访问同步代码块的场景。但是如果存在多个线程竞争同一个锁的情况,偏向锁会失效,需要升级为轻量级锁或重量级锁。

一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。

一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。

轻量级锁

轻量级锁是在 JVM 环境上面

轻量级锁在以下情况下会升级为重量级锁:

  1. 当多个线程竞争同一个锁时,如果自旋等待超过了一定的次数,轻量级锁会升级为重量级锁。
  2. 当一个线程已经持有锁,另一个线程在自旋等待获取锁,而此时又有第三个线程试图访问该锁时,轻量级锁也会升级为重量级锁。

这种升级过程是为了处理更复杂的并发情况,确保线程安全,但也会带来一定的性能开销。因此,在设计并发系统时,需要仔细考虑锁的使用和升级策略,以平衡性能和线程安全的需求。

重量级锁

重量级锁是在操作系统环境上面。重量级锁会阻塞线程。

重量级锁是指当一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。 它是依赖于底层操作系统的Mutex实现,也就是互斥锁。重量级锁会让锁从用户态切换到内核态,将线程的调度交给操作系统,因此性能相对较低。

在并发编程中,当锁的状态为轻量级锁时,若有另一个线程尝试获取锁但自旋一定次数后仍未成功,那么该锁就会升级为重量级锁,此时其他申请锁的线程会进入阻塞状态,导致性能下降。

总的来说,重量级锁是为了确保线程安全而采取的一种机制,但使用时应注意其带来的性能开销,并尽量通过合理的锁策略来避免不必要的阻塞和性能损失。

为什么 synchronized 重量级锁,效率低下

Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长",时间成本相对较高,这也是为什么早期的synchronized效率低的原因 Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。

锁的优化

JIT编译器对锁做了两方面的优化:

适应性自旋(Adaptive Spinning)

从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式------适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

锁粗化(Lock Coarsening)

锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子:

java 复制代码
public  void lockCoarsening() {
    int i=0;
    synchronized (this){
        i=i+1;
    }
    synchronized (this){
        i=i+2;
    }
}

上面的两个同步代码块可以变成一个

java 复制代码
public  void lockCoarsening() {
    int i=0;
    synchronized (this){
        i=i+1;
        i=i+2;
    }
}

锁消除(Lock Elimination)

锁消除即删除不必要的加锁操作的代码。比如下面的代码,下面的for循环完全可以移出来,这样可以减少加锁代码的执行过程

java 复制代码
public  void lockElimination() {
    int i=0;
    synchronized (this){
        for(int c=0; c<1000; c++){
            System.out.println(c);
        }
        i=i+1;
    }
}

文章部分参考

锁升级过程(无锁、偏向锁、轻量级锁、重量级锁)

相关推荐
程序猿麦小七9 分钟前
今天给在家介绍一篇基于jsp的旅游网站设计与实现
java·源码·旅游·景区·酒店
Dontla20 分钟前
Rust泛型系统类型推导原理(Rust类型推导、泛型类型推导、泛型推导)为什么在某些情况必须手动添加泛型特征约束?(泛型trait约束)
开发语言·算法·rust
张某布响丸辣21 分钟前
SQL中的时间类型:深入解析与应用
java·数据库·sql·mysql·oracle
喜欢打篮球的普通人26 分钟前
rust模式和匹配
java·算法·rust
java小吕布40 分钟前
Java中的排序算法:探索与比较
java·后端·算法·排序算法
慢生活的人。1 小时前
SpringSecurity+jwt+captcha登录认证授权总结
java·认证·rbac·权限·验证
Neophyte06081 小时前
C++算法练习-day40——617.合并二叉树
开发语言·c++·算法
向阳12181 小时前
LeetCode40:组合总和II
java·算法·leetcode
慕容复之巅1 小时前
基于MATLAB的条形码的识别图像处理报告
开发语言·图像处理·matlab
云空1 小时前
《InsCode AI IDE:编程新时代的引领者》
java·javascript·c++·ide·人工智能·python·php