synchronized 优化原理

1,841 阅读6分钟

这是我参与8月更文挑战的第28天,活动详情查看:8月更文挑战

一、对象头

首先来看下 Java 对象结构。

对象中的几个部分的作用:

  • 1、对象头中的 Mark word 主要用来表示对象的线程锁状态,另外还可以用来配合 GC、存放该对象的 hashCode
  • 2、Klass Word 是一个指向方法区中 Class 信息的指针,意味着该对象可随时知道自己是哪个 class 的实例。
  • 3、对象体是对象的成员属性

Synchronized 用的锁是存在Java对象里的,那么什么是Java 对象头呢?HotSpot虚拟机的对象头主要包括两部分数据:Mark Work(标记字段)和Klass Pointer(类型指针),虚拟机通过这个类型指针判断这个对象是哪个类的实例,Mark word默认存储对象的HashCode等运行时数据。

image-20210828090804671

Java 对象处于 5 种不同状态时,Mark Word 中 64 个位的表现形式,上面每一行对象处于某种状态的 lock 标记如下

biased_lock lock 状态

0 01 无锁

1 01 偏向锁

00 轻量级锁

10 重量级锁

11 GC标记

二、轻量级锁

轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。轻量级锁对使用者是透明的,即语法仍然是 synchronized ,假设有两个方法同步块,利用同一个对象加锁。

下面通过这个例子来说明下轻量级锁的加锁原理。

 static final Object obj = new Object();
 public static void method1() {
      synchronized( obj ) {
          // 同步块 A
          method2();
      }
 }
 public static void method2() {
      synchronized( obj ) {
          // 同步块 B
      }
 }

1、每次指向到 synchronized 代码块时,先会在栈帧中创建 锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存对象的 Mark Word 和对象引用 reference

image-20210828090121704

2、让锁记录中的 Object reference 指向对象,并且尝试用 cas(compare and sweep) 替换 Object 对象的 Mark Word ,将 Mark Word 的值存入锁记录中。一开始 的01 是无锁状态,然后把 lock Record 的 lock 状态 00 (轻量锁)与 Object 对象的 01(正常)进行 CAS 替换(CAS 是原子性操作)。

image-20210828091339240

3、如果 cas 替换成功,那么对象的对象头储存的就是锁记录的地址状态 00 表示轻量级锁,这个对象就知道是哪个线程锁住我了,如下所示

image-20210828091505742

4、如果cas失败,有两种情况

  • 如果是其它线程已经持有了该 Object 的轻量级锁,那么表示有竞争,首先会进行自旋锁,自旋一定次数后,如果还是失败就进入锁膨胀阶段。

  • 如果是自己的线程已经执行了 synchronized 进行加锁,那么再添加一条 Lock Record 作为重入的计数。只不过它在存储数据的时候会存放一个 null,代表加同个对象加锁的个数

    image-20210828092132113

5、解锁1: 当线程退出 synchronized 代码块的时候,如果获取的是取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

image-20210828092215858

6、解锁2: 当线程退出 synchronized 代码块的时候,如果获取的锁记录取值不为 null,那么使用 cas 将 Mark Word 的值恢复给对象头

  • 成功则解锁成功,对象头状态变为00
  • 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

三、 锁膨胀

前面说了,在加锁的时候发现其他线程对该对象加上了轻量级锁,这时CAS 操作失败,需要进行锁膨胀,将轻量级变为重量级锁。

1、当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁,此时加锁失败。

image-20210828092752098

2、这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,

  • 既为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址。
  • 然后Thread-1自己进入 Monitor 的 EntryList Blocked 中阻塞。

image-20210828093203352

3、当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给 对象头,因为 Object 指向的是 Monitor ,就会失败,所以会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为null,唤醒 EntryList 中 BLOCKED 线程,Thread-1 此时就参与重量级锁的竞争。

四、 自旋优化

重量级锁竞争时,还可以使用自旋来进行优化,就是让阻塞的线程循环查看几次,等待一段次数后再进入阻塞,如果当前线程自旋成功(即这时候锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

  • 在Java 6 之后 自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就会自旋几次;反之,就会少自旋甚至不自旋。
  • java 7 之后不能控制是否开启自旋功能。

五、 偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程ID 是自己的就表示没有竞争,不用重新 CAS,以后只要不发生竞争,这个对象就归该线程所有。

image-20210828213820121

5.1 偏向锁获取

“偏向”的意思是假定将来只有第一个申请锁的线程会使用锁(不会有任何线程来申请锁)。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程ID,以后再进入退出的时候,直接把当前线程ID和Mark word的存储的线程id进行比较。一致就进入。如果Mark word的偏向锁的标识设置为1(当前是偏向锁),还是不死心,则尝试使用CAS去将Mark Word记录的线程id替换为自己的当前id。如果为0的话,表示当前无锁,用CAS去竞争锁。

5.2 偏向锁撤销

偏向锁使用了一种等待竞争出现才会释放锁的机制,所以当其他线程尝试竞争偏向锁的时候,持有偏向锁的线程才会释放锁。

1、偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)

2、暂停持有偏向锁的线程,判断锁对象是否还处于被锁定状态。

3、要么恢复到无锁(01)要么恢复到轻量级锁的状态(00)