您现在的位置是:首页 > 编程 > 

解"锁"疑惑:偏向锁为什么不是锁?锁升级又是什么?何时禁用偏向锁和轻量级锁?重量级锁怎么回事?

2025-07-29 11:30:57
解"锁"疑惑:偏向锁为什么不是锁?锁升级又是什么?何时禁用偏向锁和轻量级锁?重量级锁怎么回事? 大家好,我是程序视点的小二哥!今天我们继续来聊聊Java中的锁!解"锁"疑惑:偏向锁为什么不是锁?锁升级又是发生的?何时禁用偏向锁和轻量级锁?synchronized怎么用?锁是什么?偏向锁是什么?锁如何升级?何为膨胀?自旋锁何解?互斥锁怎么来的?何时要禁用偏向锁

解"锁"疑惑:偏向锁为什么不是锁?锁升级又是什么?何时禁用偏向锁和轻量级锁?重量级锁怎么回事?

大家好,我是程序视点的小二哥!今天我们继续来聊聊Java中的锁!

解"锁"疑惑:偏向锁为什么不是锁?锁升级又是发生的?何时禁用偏向锁和轻量级锁?

  • synchronized怎么用?
  • 锁是什么?
  • 偏向锁是什么?
  • 锁如何升级?何为膨胀?
  • 自旋锁何解?
  • 互斥锁怎么来的?
  • 何时要禁用偏向锁和轻量级锁?

带着上面疑问,我们一起来解“锁”疑惑!以下是第二篇文章来讲,方便大家记忆!欢迎持续关注【程序视点】,这样就不会错过之后的精彩内容啦!

前言

前面说过,在大多数情况下,总是同一个线程去访问同步块代码,基于这样一个假设,引入了偏向锁,只需要用一个CAS操作和简单地判断比较,就可以让一个线程持续地拥有一个锁。

也正因为此假设,在Jdk1.6中,偏向锁的开关是默认开启的,适用于只有一个线程访问同步块的场景。

但一旦出现竞争,也即有另外一个线程也要来访问这一段代码,偏向锁就不适用于这种场景了。

锁膨胀

如果两个线程都是活跃的,会发生竞争,此时偏向锁就会发生升级,也就是我们常常听到的锁膨胀。

偏向锁会膨胀成轻量级锁(lightweight locking)。

锁撤销

偏向锁有一个不好的点就是,一旦出现多线程竞争,需要升级成轻量级锁,是有可能需要先做出锁撤销的操作。

而锁撤销的操作,相对来说,开销就会比较大,其步骤如下:

  1. 在一个安全点停止拥有锁的线程,就跟开始做GC操作一样。
  2. 遍历线程栈,如果存在锁记录的话,需要修复锁记录和Markword,使其变成无锁状态。
  3. 唤醒当前线程,将当前锁升级成轻量级锁。
轻量级锁

而本质上呢,其实就是锁对象头中的Markword内容又要发生变化了。

下面先简单地描述其膨胀的步骤:

  1. 线程在自己的栈桢中创建锁记录 LockRecord
  2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中
  3. 将锁记录中的Owner指针指向锁对象
  4. 将锁对象的对象头的MarkWord替换为指向锁记录的指针。

同样,我们还是利用Java开发人员提供的一张图来描述此步骤:

可以根据上面两图来印证上面几个步骤,但在这里,其实对象的Markword其实也是发生了变化的,其现在的内容结构如下:

bit fields

锁标志位

指向LockRecord的指针

00

说到这里,我们又通过偏向锁引入了轻量级锁的概念,那么轻量级锁是怎么个轻量级法,它具体的实现又是怎么样的呢?

就像偏向锁的前提,是同步代码块在大多数情况下只有同一个线程访问的时候。

而轻量级锁的前提则是,线程在同步代码块里面的操作非常快,获取锁之后,很快就结束操作,然后将锁释放出来。

但是不管再怎么快,一旦一个线程获得锁了,那么另一个线程同时也来访问这段代码时,怎么办呢?这就涉及到我们下面所说的锁自旋的概念了。

自旋锁/自适应自旋锁

来到轻量级锁,其实轻量级的叙述就来自于自旋的概念。

因为前提是线程在临界区的操作非常快,所以它会非常快速地释放锁,所以只要让另外一个线程在那里地循环等待,然后当锁被释放时,它马上就能够获得锁,然后进入临界区执行,然后马上又释放锁,让给另外一个线程。

所谓自旋,就是线程在原地空循环地等待,不阻塞,但它是消耗CPU的。

所以对于轻量级锁,它也有其限制所在:

  1. 因为消耗CPU,所以自旋的次数是有限的,如果自旋到达一定的次数之后,还获取不到锁,那这种自旋也就无意义。但在上述的前提下,这种自旋的次数还是比较少的(经验数据)。当然,一开始的自旋次数都是固定的,但是在经验代码中,获得锁的线程通常能够马上再获得锁,所以又引入了自适应的自旋,即根据上次获得锁的情况和当前的线程状态,动态地修改当前线程自旋的次数。
  2. 当另一个线程释放锁之后,当前线程要能够马上获得锁,所以如果有超过两个的线程同时访问这段代码,就算另外一个线程释放锁之后,当前线程也可能获取不到锁,还是要继续等待,空耗CPU。

从以上两点可以看出,当线程通过自旋获取不到锁了,比如临界区的操作太花时间了,或者有超过2个以上的线程在竞争锁了,轻量级锁的前提又不成立了。当虚拟机检查到这种情况时,又开始了膨胀的脚步。

互斥锁(重量级锁)

相比起轻量级锁,再膨胀的锁,一般称之为重量级锁,因为是依赖于每个对象内部都有的monitor锁来实现的,而monitor又依赖于操作系统的MutexLock(互斥锁)来实现,所以一般重量级锁也叫互斥锁。

由于需要在操作系统的内核态和用户态之间切换的,需要将线程阻塞挂起,切换线程的上下文,再恢复等操作,所以当synchronized升级成互斥锁,依赖monitor的时候,开销就比较大了,而这也是之前为什么说synchronized是一个很重的操作的原因了。

当然,升级成互斥锁之后,锁对象头的Markword内容也是会变化的,其内容如下:

bit fields

锁标志位

指向Mutex的指针

10

每次检查当前线程是否获得锁,其实就是检查Mutex的值是否为0,不为0,说明其为其线程所占有,此时操作系统就会介入,将线程阻塞,挂起,释放CPU时间,等待下一次的线程调度。

好了,到这里,对于synchronized所修改的同步方法或者同步代码块,虚拟机是如何操作的,大家应该也有一个简单的印象了。

当使用synchronized关键字的时候,在java1.6之后,根据不同的条件和场景,虚拟机是一步一步地将偏向锁升级成轻量级锁,再最终升级成重量级锁的,而这个过程是不可逆的,因为一旦升级成重量级锁,则说明偏向锁和轻量级锁是不适用于当前的应用场景的,那再降级回去也没什么意义。

从这一点,也可以看出,如果我们的应用场景本身就不适用于偏向锁和轻量级锁,那么我们在程序一开始,就应该禁用掉偏向锁和轻量级锁,直接使用重量级锁,省去无谓的开销。

总结

在这里总结一下,在使用synchronized关键字的时候,本质上是否获得锁,是通过修改锁对象头中的markword的内容来标记是否获得锁,并由虚拟机来根据具体的应用场景来锁进行升级。

简单地将上述几个零散的markword变化合在一起,展示在下面:

锁状态

bits

1bit是否是偏向锁

2bit锁标志位

无锁状态

对象的hashCode

0

01

偏向锁

线程ID

1

01

轻量级锁

指向栈中锁记录的指针

0

00

重量级锁

指向互斥量的指针

0

10

最后

【程序视点】助力打工人减负,从来不是说说而已!

后续小二哥会继续详细分享更多实用的工具和功能。持续关注,这样就不会错过之后的精彩内容啦!

如果这篇文章对你有帮助的话,别忘了【点赞】【分享】支持下哦~

#感谢您对电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格的认可,转载请说明来源于"电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格

本文地址:http://www.dnpztj.cn/biancheng/1247724.html

相关标签:无
上传时间: 2025-07-27 12:31:17
留言与评论(共有 7 条评论)
本站网友 文件管理器
16分钟前 发表
需要将线程阻塞挂起
本站网友 注射丰胸
16分钟前 发表
将线程阻塞
本站网友 焦虑神经症
8分钟前 发表
当使用synchronized关键字的时候
本站网友 中国旅游日
6分钟前 发表
那这种自旋也就无意义
本站网友 激光脱毛的价位
27分钟前 发表
它具体的实现又是怎么样的呢? 就像偏向锁的前提
本站网友 乳加力
14分钟前 发表
其步骤如下: 在一个安全点停止拥有锁的线程