大家好,我是程序视点的小二哥!今天我们继续来聊聊Java中的锁!
探“锁”源头:synchronized、偏向锁与锁膨胀的秘密! - 简书 (jianshu.com)
synchronized怎么用?
锁是什么?
偏向锁是什么?
锁如何升级?何为膨胀?
自旋锁何解?
互斥锁怎么来的?
何时要禁用偏向锁和轻量级锁?
带着上面疑问,我们一起来解“锁”疑惑!以下是第二篇文章来讲,方便大家记忆!欢迎持续关注【程序视点】,这样就不会错过之后的精彩内容啦!
前言
前面说过,在大多数情况下,总是同一个线程去访问同步块代码,基于这样一个假设,引入了偏向锁,只需要用一个CAS操作和简单地判断比较,就可以让一个线程持续地拥有一个锁。 也正因为此假设,在Jdk1.6中,偏向锁的开关是默认开启的,适用于只有一个线程访问同步块的场景。
但一旦出现竞争,也即有另外一个线程也要来访问这一段代码,偏向锁就不适用于这种场景了。
锁膨胀
如果两个线程都是活跃的,会发生竞争,此时偏向锁就会发生升级,也就是我们常常听到的锁膨胀。 偏向锁会膨胀成轻量级锁(lightweight locking)。
锁撤销
偏向锁有一个不好的点就是,一旦出现多线程竞争,需要升级成轻量级锁,是有可能需要先做出锁撤销的操作。
而锁撤销的操作,相对来说,开销就会比较大,其步骤如下:
在一个安全点停止拥有锁的线程,就跟开始做GC操作一样。
遍历线程栈,如果存在锁记录的话,需要修复锁记录和Markword,使其变成无锁状态。
唤醒当前线程,将当前锁升级成轻量级锁。
轻量级锁
而本质上呢,其实就是锁对象头中的Markword内容又要发生变化了。
下面先简单地描述其膨胀的步骤:
线程在自己的栈桢中创建锁记录 LockRecord
将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中
将锁记录中的Owner指针指向锁对象
将锁对象的对象头的MarkWord替换为指向锁记录的指针。
同样,我们还是利用Java开发人员提供的一张图来描述此步骤:
可以根据上面两图来印证上面几个步骤,但在这里,其实对象的Markword其实也是发生了变化的,其现在的内容结构如下:
说到这里,我们又通过偏向锁引入了轻量级锁的概念,那么轻量级锁是怎么个轻量级法,它具体的实现又是怎么样的呢?
就像偏向锁的前提,是同步代码块在大多数情况下只有同一个线程访问的时候。
而轻量级锁的前提则是,线程在同步代码块里面的操作非常快,获取锁之后,很快就结束操作,然后将锁释放出来。
但是不管再怎么快,一旦一个线程获得锁了,那么另一个线程同时也来访问这段代码时,怎么办呢?这就涉及到我们下面所说的锁自旋的概念了。
自旋锁/自适应自旋锁
来到轻量级锁,其实轻量级的叙述就来自于自旋的概念。
因为前提是线程在临界区的操作非常快,所以它会非常快速地释放锁,所以只要让另外一个线程在那里地循环等待,然后当锁被释放时,它马上就能够获得锁,然后进入临界区执行,然后马上又释放锁,让给另外一个线程。
所谓自旋,就是线程在原地空循环地等待,不阻塞,但它是消耗CPU的。
所以对于轻量级锁,它也有其限制所在:
因为消耗CPU,所以自旋的次数是有限的,如果自旋到达一定的次数之后,还获取不到锁,那这种自旋也就无意义。但在上述的前提下,这种自旋的次数还是比较少的(经验数据)。当然,一开始的自旋次数都是固定的,但是在经验代码中,获得锁的线程通常能够马上再获得锁,所以又引入了自适应的自旋,即根据上次获得锁的情况和当前的线程状态,动态地修改当前线程自旋的次数。
当另一个线程释放锁之后,当前线程要能够马上获得锁,所以如果有超过两个的线程同时访问这段代码,就算另外一个线程释放锁之后,当前线程也可能获取不到锁,还是要继续等待,空耗CPU。
从以上两点可以看出,当线程通过自旋获取不到锁了,比如临界区的操作太花时间了,或者有超过2个以上的线程在竞争锁了,轻量级锁的前提又不成立了。当虚拟机检查到这种情况时,又开始了膨胀的脚步。
互斥锁(重量级锁)
相比起轻量级锁,再膨胀的锁,一般称之为重量级锁,因为是依赖于每个对象内部都有的monitor锁来实现的,而monitor又依赖于操作系统的MutexLock(互斥锁)来实现,所以一般重量级锁也叫互斥锁。
由于需要在操作系统的内核态和用户态之间切换的,需要将线程阻塞挂起,切换线程的上下文,再恢复等操作,所以当synchronized升级成互斥锁,依赖monitor的时候,开销就比较大了,而这也是之前为什么说synchronized是一个很重的操作的原因了。 当然,升级成互斥锁之后,锁对象头的Markword内容也是会变化的,其内容如下:
每次检查当前线程是否获得锁,其实就是检查Mutex的值是否为0,不为0,说明其为其线程所占有,此时操作系统就会介入,将线程阻塞,挂起,释放CPU时间,等待下一次的线程调度。
好了,到这里,对于synchronized所修改的同步方法或者同步代码块,虚拟机是如何操作的,大家应该也有一个简单的印象了。 当使用synchronized关键字的时候,在java1.6之后,根据不同的条件和场景,虚拟机是一步一步地将偏向锁升级成轻量级锁,再最终升级成重量级锁的,而这个过程是不可逆的,因为一旦升级成重量级锁,则说明偏向锁和轻量级锁是不适用于当前的应用场景的,那再降级回去也没什么意义。
从这一点,也可以看出,如果我们的应用场景本身就不适用于偏向锁和轻量级锁,那么我们在程序一开始,就应该禁用掉偏向锁和轻量级锁,直接使用重量级锁,省去无谓的开销。
总结
在这里总结一下,在使用synchronized关键字的时候,本质上是否获得锁,是通过修改锁对象头中的markword的内容来标记是否获得锁,并由虚拟机来根据具体的应用场景来锁进行升级。
简单地将上述几个零散的markword变化合在一起,展示在下面:
最后
【程序视点】助力打工人减负,从来不是说说而已!
后续小二哥会继续详细分享更多实用的工具和功能。持续关注,这样就不会错过之后的精彩内容啦!
如果这篇文章对你有帮助的话,别忘了【点赞】【分享】支持下哦~