面试官:说一下synchronized锁升级过程

目录

1.前言

2.对象头

   (1)无锁

   (2)偏向锁

   (3)轻量级锁

   (4)重量级锁

3.对象体(实例数据)

4.对齐字节

5.整体锁升级过程

6.举例说明


1.前言

   首先,synchronized 是什么?

synchronized是一种对象锁(锁的是对象而非引用),作用粒度是对象,java中每个对象都可以上锁(同一时间只有一个线程能上锁成功),而且通过对象内部存储的markword标记锁状态。 synchronized加锁方式

  • 修饰静态方法: 锁住当前 class,作用于该 class 的所有实例。
  • 修饰非静态方法: 只会锁住当前 class 的实例。
  • 修饰代码块: 该方法接受一个对象作为参数,锁住的即该对象。

对于熟悉的人来说,可能会想:

不就是「无锁 ==> 偏向锁 ==> 轻量级锁 ==> 重量级锁 」吗?

但你真的知道无锁、偏向锁、轻量级锁、重量级锁到底代表着什么吗?这些锁存储在哪里?以及什么情况下会使得锁向下一个 level 升级?

我们似乎必须先搞清楚 Java 内置锁,其内部结构是啥样的?内置锁又存放在哪里?

在 Java 中,每个对象中都隐藏着一把锁,而 synchronized 关键字就是激活这把隐式锁的把手(开关)。所以java内置锁存放在 Java 对象中

那么现在的问题就从内置锁结构是啥变成了Java 对象是什么?

面试官:说一下synchronized锁升级过程_第1张图片

接下来分别深入讨论一下这三部分。

2.对象头

可以看出,其由 Mark Word、Class Pointer、数组长度三个字段组成。简单来说:

  • Mark Word: 主要用于存储自身运行时数据。
  • Class Pointer: 是指针,指向方法区中该 class 的对象,JVM 通过此字段来判断当前对象是哪个类的实例。
  • 数组长度: 当且仅当对象是数组时才会有该字段。

Class Pointer 和数组长度没什么好说的,接下来重点聊聊 Mark Word

Mark Word 所代表的「运行时数据」主要用来表示当前 Java 对象的线程锁状态以及 GC 的标志。而线程锁状态分别就是无锁、偏向锁、轻量级锁、重量级锁。

所以前文提到的这 4 个状态,其实就是 Java 内置锁的不同状态。

在 JDK 1.6 之前,内置锁都是重量级锁,效率低下。效率低下表现在

而在 JDK 1.6 之后为了提高 synchronized 的效率,才引入了偏向锁、轻量级锁。

 随着锁竞争逐渐激烈,其状态会按照「无锁 ==> 偏向锁 ==> 轻量级锁 ==> 重量级锁 」这个方向逐渐升级,并且不可逆,只能进行锁升级,而无法进行锁降级。

 (1)无锁

这个可以理解为单线程很快乐的运行,没有其他的线程来和其竞争。

 (2)偏向锁

首先,什么叫偏向锁?

当只有一个线程获得了锁,锁就进入偏向模式,MarkWord标识偏向状态,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作。这样降低了获取锁的代价,提升了效率。

看到这里,你会发现无锁、偏向锁的 lock 标志位是一样的,即都是 01,这是因为无锁、偏向锁是靠字段 biased_lock 来区分的,0 代表没有使用偏向锁,1 代表启用了偏向锁。为什么要这么搞?你可以理解为无锁、偏向锁在本质上都可以理解为无锁(参考上面提到的线程 A 的状态),所以 lock 的标志位都是 01 是没问题的。

 (3)轻量级锁

当有其它线程要获取锁,竞争不是很激烈,锁进入轻量级锁,MarkWord标识轻量级状态,此时等待锁的线程开始自旋,即空循环等待锁释放,此过程不释放cpu。如果循环几次,其他的线程释放了锁,就不需要进行用户态到内核态的切换。虽然如此,但自旋需要占用很多 CPU 的资源(自行理解汽车空档疯狂踩油门)。如果另一个线程 一直不释放锁,难道它就在这一直空转下去吗?

当然不是,JDK 1.7 之前是普通自旋,会设定一个最大的自旋次数,默认是 10 次,超过这个阈值就停止自旋。JDK 1.7 之后,引入了适应性自旋。简单来说就是:这次自旋获取到锁了,自旋的次数就会增加;这次自旋没拿到锁,自旋的次数就会减少。

(4)重量级锁

当获取锁的竞争变的激烈,比如来了很多个线程或者某个线程自旋等待的次数太多了,锁进入重量级锁,当其膨胀成重量级锁后,其他竞争的线程进来就不会自旋了,而是直接阻塞等待,并且 Mark Word 中的内容会变成一个监视器(monitor)对象,用来统一管理排队的线程。

MarkWord标识重量级状态,重量级锁依赖操作系统的Mutex lock实现,此时等待锁的线程挂起,当锁释放后再由操作系统唤醒重新尝试获取锁,由于借助操作系统,导致用户态内核态切换,此过程时间成本比较高。

原始的synchronized是直接使用重量级锁,才会导致性能很低,加入锁升级才使得synchronized性

3.对象体(实例数据)

对象体包含了当前对象的字段和值,在业务中u l是较为核心的部分。

4.对齐字节

就是单纯用于填充的字节,没有其他的业务含义。其目的是为了保证对象所占用的内存大小为 8 的倍数,因为HotSpot VM 的内存管理要求对象的起始地址必须是 8 的倍数。

5.整体锁升级过程

了解完 4 种锁状态之后,我们就可以整体的来看一下锁升级的过程了。

面试官:说一下synchronized锁升级过程_第2张图片

线程A进入 synchronized 开始抢锁,JVM 会判断当前是否是偏向锁的状态,如果是就会根据 Mark Word 中存储的线程 ID 来判断,当前线程A是否就是持有偏向锁的线程。如果是,则忽略 check,线程A直接执行临界区内的代码。

但如果 Mark Word 里的线程不是线程 A,就会通过自旋尝试获取锁,如果获取到了,就将 Mark Word 中的线程 ID 改为自己的;如果竞争失败,就会立马撤销偏向锁,膨胀为轻量级锁。

后续的竞争线程都会通过自旋来尝试获取锁,如果自旋成功那么锁的状态仍然是轻量级锁。然而如果竞争失败,锁会膨胀为重量级锁,后续等待的竞争的线程都会被阻塞。

 6.举例说明

假设一个公司有多个会议室,每个部门需要获取到会议室的锁才能进去开会,会议室门口挂着一个写字板,时刻记录当前会议室使用状态。

  • 会议室相当于对象
  • 团队相当于线程
  • 会议室的锁相当于对象的锁
  • 写字板相当于MarkWord

1.无锁

会议室无部门使用

2.偏向锁

公司发现大部分时间,同一个会议室都是同一个部门占用,于是当A部门第一次占用会议室时,在写字板上写上偏向 A部门,下次A部门进入不用修改就可以直接进入会议室,大大提升了开会效率。

如果B部门想使用会议室,此时A部门已经不使用该会议室,则修改写字板偏向 B部门。

这就是偏向锁,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁带来的开销,所以引入偏向锁。

3.轻量级锁

如果B部门想使用会议室,此时A还占用着会议室(写字板上记录偏向 A部门),此时出现了竞争,写字板上修改为轻量竞争,B部门哪也不去,就在会议室外原地等待(自旋),因为公司大部分会议时间都很短,B相信A一般会很快出来。

如果A确实一会就出来了,B马上去抢会议室的锁。

这就是轻量级锁,偏向锁出现了竞争会升级为轻量级锁,因为大部分线程占用锁的时间不会特别长,所以等待线程刚开始不需要挂起,只需要通过空转自旋等待,一般很快就会获取到锁,但是这个过程一直占用着cpu。

4.重量级锁

上面的情况,如果B等了很久A都不出来,或者这段时间公司特别繁忙,各部门频繁开会,还有C,D,E…等等部门也要使用该会议室,这时如果A在里面开会没完没了,其它团队一直在外面傻转着也不是事。

这时候就要请会议室管理员帮忙了,他让各部门都回去吧,写字板上修改为重量竞争,等A团队开完会出来,我负责通知其它团队,你们再过来抢会议室的锁。

这样在会议室竞争特别激烈时,请会议室管理员帮忙有效的避免了等待团队傻等,但如果在竞争不激烈的情况下就没有必要请出会议室管理员,毕竟造成额外开销,而且靠会议室管理员通知再来抢会议室肯定比站会议室外面等要慢很多。

这就是重量级锁,其中会议室管理员相当于操作系统,当某个线程自旋次数过多或者多个线程同时竞争锁,锁竞争变的激烈,轻量级锁升级为重量级锁,此时等待线程都挂起,对象锁释放后再由操作系统唤醒线程,此过程开销很大。

synchronized最开始就是不管竞争激不激烈都使用重量级锁导致性能很低,但竞争激励时如果任由等待线程空转消耗跟大,所以竞争激励升级为重量级锁也是非常合理。

你可能感兴趣的:(java,java-ee,jvm)