Java并发偏向锁、轻量级锁、重量级锁、synchronized和volatile的实现原理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xyh930929/article/details/84571805

偏向锁、轻量级锁和重量级锁之间的关系,首先打个比方:假设现在厕所只有一个位置,每个使用者都有打开门锁的钥匙。必须打开门锁才能使用厕所。

  1. 小明今天吃坏了东西需要反复去厕所,如果小明每次都要开锁就很耽误时间,于是门锁将小明的脸记录下来(假设那个锁是智能锁),下次小明再来的时候门锁会自动识别出是小明来了,然后自动开锁,这样就省去了小明拿钥匙开门的过程,此时门锁就是偏向锁,也可以理解为偏向小明的锁。

  2. 接下来,小红又去上厕所,试图将厕所的门锁设置为偏向自己的偏向锁,于是发现门锁无法偏向自己,因为此时门锁已是偏向小明的偏向锁。于是小红很生气,要求门锁撤销对小明的偏向,当然,小明也不同意门锁偏向小红。于是等小明用完厕所之后,门锁撤销了对任何人的偏向(只要出现竞争的情况,就会撤销偏向锁)。这个过程就是撤销偏向锁。此时门锁升级为轻量级锁

  3. 等小明出来以后,轻量级锁正式生效 。下一次小明和小红同时来厕所,谁跑的快谁先走到门前,开门后将门锁拿进厕所,并将门锁打开以后拿进厕所里,将门反锁,于是在门外原来放门锁的位置放置了一个“有人”的标志(这个标识可以理解为指向门锁的指针,或者理解为作为锁的Java对象头的Mark Word值),这时,小红看到有人以后很着急,想趁着里面的人出来时马上进去,于是不断的来敲门,问小明什么时候出来。这个过程就是自旋

  4. 反复敲了几次以后,小明受不了了,对小红喊话,说你别敲了,等我用完厕所我告诉你,于是小红去一边等着(线性阻塞)。此时门锁升级为重量级锁。升级为重量级锁的后果就是,小红不再反复敲门,小明在上完厕所以后必须告诉小红一声,否则小红就会一直等着。

几种锁的对比:偏向锁在只有一个人上厕所时非常高效,省去了开门的过程。轻量级锁在有多人上厕所但是每个人使用的特别快的时候,比较高效,因为会出现这种现象,小红敲门的时候正好赶上小明出来,这样就省得小明出来告诉小红以后小红才能进去,但是这样可能会出现小红敲门失败的情况(就是敲门时小明还没用完)。重量级锁相比与轻量级锁的多了一步小明呼唤小红的步骤,但是却省掉了小红反复去敲门的过程,但是能保证小红去厕所时厕所一定是没人的。

将上面例子中的小明、小红理解为两个线程,上厕所理解为执行同步代码,门锁理解为同步代码的锁。就会得到三种锁的优缺点以及适用场景,如下:

在这里插入图片描述

上面的例子是我自己对于三种锁的理解,下面是读“Java并发编程的艺术”那本书的学习整理。

1. 上下文切换

CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换

2. volatile

Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行。被volatile修饰的变量进行写的操作转化的汇编指令前面会加上lock前缀。Lock前缀的指令在多核处理器下会引发两件事:

  • Lock前缀指令会引起处理器缓存回写到内存。
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

注意:volatile只能保证可见性,无法保证原子性。

3. synchronized

Java中的每一个对象都可以作为synchronized的锁。

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象。

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。synchronized用的锁是存在Java对象头里的。

前面提到例子中的,就是被用在同步代码的部分。锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

Java对象头的结构:
在这里插入图片描述
上图中的MarkWord存储结构:
在这里插入图片描述

在当前对象被设置为锁时,Mark Word可能变化为存储以下4种数据:
在这里插入图片描述

3.1 偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

撤销偏向锁:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

不启用偏向锁:偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

3.2 轻量级锁和重量级锁

轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级锁解锁:轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争夺锁,导致锁膨胀(锁升级)的流程图。

在这里插入图片描述

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

猜你喜欢

转载自blog.csdn.net/xyh930929/article/details/84571805