synchronized原理及锁膨胀

一、对象头

​ 在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头,实例数据和对齐填充,这里我们就先介绍一下对象头。

​ 在HotSpot虚拟机的对象头部分包括三类信息:

  • 第一类是用于存储对象自身的运行时数据,如哈希吗,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间轴等,这部分的数据的长度在32位和64位的虚拟机中(未开启压缩指针)中分别为32个比特和64个比特。官方称之为"Mark Word"。
  • 对象头的另一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。(并不是所有的虚拟机实现都必须在对象数据上保留类型指针)
  • 如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。

二、对象头的格式

32位虚拟机

  • 普通对象
|--------------------------------------------------------------|
|					 Object Header (64 bits) 				   |
|------------------------------------|-------------------------| 
| Mark Word (32 bits) 	  			 |  Klass Word (32 bits)   |
|------------------------------------|-------------------------|
  • 数组对象(多一个数组长度)
|---------------------------------------------------------------------------------|
|					 			Object Header (96 bits) 						  |
|--------------------------------|-----------------------|------------------------|
| 		Mark Word(32bits) 		 | Klass Word(32bits) 	 |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|

其中对象头Mark Word(32为虚拟机)的结构为:

|--------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                   |       State        |
|--------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:0 | lock:01 |       Normal       |
|--------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:01 |       Biased       |
|--------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:00 | Lightweight Locked |
|--------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:10 | Heavyweight Locked |
|--------------------------------------------------------|--------------------|
|                                              | lock:11 |    Marked for GC   |
|--------------------------------------------------------|--------------------|

其中各个部分的含义如下:

**lock:**为2位的锁状态标记位由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。

biased_lock lock 状态
0 01 无锁
1 01 偏向锁
0 00 轻量级锁
0 10 重量级锁
0 11 GC标记

biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。

age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。

thread:持有偏向锁的线程ID。
epoch:偏向时间戳。
ptr_to_lock_record:指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:指向管程Monitor的指针。

对于64位的虚拟机其Mark Word格式如下:

|--------------------------------------------------------------------|--------------------|
| 					 Mark Word (64 bits) 							 | 		  State 	  |
|--------------------------------------------------------------------|--------------------|
| unused:25    | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | 		  Normal	  |
|--------------------------------------------------------------------|--------------------|
| thread:54    | epoch:2	 | unused:1 | age:4 | biased_lock:1 | 01 | 		  Biased	  |
|--------------------------------------------------------------------|--------------------|
| 					ptr_to_lock_record:62						| 00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
| 			ptr_to_heavyweight_monitor:62 						| 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| 																| 11 | 	  Marked for GC	  |
|--------------------------------------------------------------------|--------------------|

三、Monitor

3.1、概述

​ Monitor 被翻译为监视器管程,Monitor 的重要特点是,同一个时刻,只有一个 进程/线程 能进入 monitor 中定义的临界区,这使得 monitor 能够达到互斥的效果。每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针

3.2、Monitor的结构

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。

image-20201007173243767

  • 刚开始 Monitor 中 Owner 为 null当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner

  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED(阻塞)

  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的

  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足(线程调用 wait() 方法)进入 WAITING 状态的线程需要被notify唤醒才能再次尝试获取锁。

注意:

  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果

  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

三、synchronized简单原理

​ synchronized同步代码块原理:

public class SynchronizedCode {
    
    
    static final Object lock = new Object();
    static int counter = 0;
    public static void main(String[] args) {
    
    
        synchronized (lock) {
    
    
            counter++;
        }
    }
}

以上代码编译过后通过Idea的jclasslib查看main()方法的字节码如下:

 0 getstatic #2 <com/juc/synchronizedcode/SynchronizedCode.lock>
 3 dup
 4 astore_1
 5 monitorenter    # 进入同步方法,当前线程尝试获取对象锁
 6 getstatic #3 <com/juc/synchronizedcode/SynchronizedCode.counter>
 9 iconst_1
10 iadd
11 putstatic #3 <com/juc/synchronizedcode/SynchronizedCode.counter>
14 aload_1
15 monitorexit   # 退出同步方法,当前线程释放对象锁
16 goto 24 (+8)
19 astore_2
20 aload_1
21 monitorexit  # 退出同步方法,当前线程释放对象锁
22 aload_2
23 athrow
24 return

有两个退出同步方法的语句是因为为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行。多的那一个就是异常结束时被执行的释放monitor 的指令。

方法级别的 synchronized 不会在字节码指令中有所体现,它是隐式的,无需同果字节吗指令看来控制

public class SynchronizedCode {
    
    
    public static void main(String[] args) {
    
    

    }
    public static  synchronized void test(){
    
    
            System.out.println("静态代码块!");
    }
}

字节码如下:

image-20201007195300461

四、synchronized锁膨胀过程

synchronized的锁膨胀是jdk1.6对synchronized的优化,锁的状态总共有四种,**无锁,偏向锁,轻量级锁,重量级锁。**锁的升级是单向的,只能从低级的锁升级到高级的锁。锁的膨胀过程为无锁->偏向锁->轻量级锁->重量级锁。

  1. 无锁:当前没有线程获取到同步监视器就是无锁状态

  2. 偏向锁:轻量级锁在没有竞争时(就自己一个线程在获取锁),每次重入仍然需要执行 CAS 操作,所以Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,此时Mark Word 的结构也变为偏向锁结构,之后当这个线程再次请求锁时,发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。

    image-20201007202227730

    1. 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0。
    2. 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟。
    3. 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

  3. 轻量级锁:倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级的锁。如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是同步周期没有竞争),那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是 synchronized。但如果存在同一时间多个线程访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

    • 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

    image-20201007201720652

    • 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录。

      image-20201007201742290

    • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下

      image-20201007201801998

    • 如果 cas 失败,有两种情况

      1. 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程

      2. 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

        image-20201007201850522

    • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一。

      image-20201007201914742

    • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头,若成功,则解锁成功,若失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁的解锁流程。

  4. 重量级锁:如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

    1. 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

      image-20201007203125120

    2. 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED。

      image-20201007203211871

    3. 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程。

  5. 自旋锁优化:重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。因为线程状态的切换比较耗时。

    自旋重试成功:

    image-20201007210323284

    自旋重试失败:

    image-20201007210449713

    1. 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
    2. 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋。
    3. Java 7 之后不能控制是否开启自旋功能。

    6.锁消除:消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。

    //以下方法,同步代码块的锁对象为局部变量,不可能存在竞争
    public void b() throws Exception {
          
          
     Object o = new Object();
     synchronized (o) {
          
          
     x++;
     }
    }
    

参考链接:https://blog.csdn.net/javazejian/article/details/72828483(绝对大佬,十分完善!)

参考链接:https://www.jianshu.com/p/3d38cba67f8b
参考资料:深入理解Java虚拟机第三版和黑马

猜你喜欢

转载自blog.csdn.net/qq_44134480/article/details/108956897
今日推荐