synchronized 原理 锁升级 详解

synchronized的作用 就是在多线程中实现锁的作用,保证synchronized锁住的代码只能有一个线程执行。synchronized用的锁是存在Java对象头里的。

对象头

在JVM中,对象在内存中的布局分为3块:对象头、实例数据和对齐填充。

  • 实例数据 : 程序代码中定义的各种类型的字段内容。
  • 对齐填充 : JVM要求对象的大小必须是8个字节的整数倍,对象头已经是8的整数倍了,如果实例数据没有8的整数倍就需要对齐填充来补全。

对象头 = Mark Word + 类型指针( Klass pointer )。

类型指针( Klass pointer : 用于标识JVM通过这个指针来确定这个对象是哪个类的实例。

Mark Word : 用于储存对象自身的运行时数据,例如对象的hashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程的ID、偏向时间戳等。 在运行期间,Mark Word里存储的数据会随着内部锁标志位的变化而变化。

无锁状态(01)

对象头开辟 25bit 的空间用来存储对象的 hashcode ,4bit 用于存放分代年龄,1bit 用来存放是否偏向锁的标识位,2bit 用来存放锁标识位为01。

偏向锁状态(01)

还是开辟25bit 的空间,其中23bit 用来存放线程ID,2bit 用来存放 epoch,4bit 存放分代年龄,1bit 存放是否偏向锁标识, 0表示无锁,1表示偏向锁,锁的标识位还是01。

轻量级锁状态(00)

开辟 30bit 的空间存放指向 栈中锁记录 的指针,2bit 存放锁的标志位,其标志位为00。

重量级锁状态(10)

30bit 的空间用来存放指向 重量级锁 的指针,2bit 存放锁的标识位,为10。

GC标记(11)

开辟30bit 的内存空间却没有占用,2bit 空间存放锁标志位为11。


之所以这些标志位会变化,是JVM对重量级锁的一种优化,在jdk1.6 之前,用synchronized 都是重量级锁 ,都需要底层操作系统的Mutex Lock(互斥锁)来实现,这个是涉及到系统调用的,会导致内核空间与用户空间的上下文切换,很低效。切换原理:

罗政:CPU上下文切换原理

于是jdk1.6之后为了增加性能减少操作Mutex Lock,引入了 偏向锁 轻量级锁 :锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。

锁升级过程


Object lock = new Object();
// 临界区之前
synchronized(lock){
  // 临界区
}
// 临界区之后

无锁

当有线程进入临界区之前(synchronized块之前),lock里的对象头的 Mark Word 结构为下面所示:

锁标志位01,无偏向。这是JVM给的初始值。

偏向锁

线程A 刚进入临界区时(synchronized),发现标志位是01 ,而且无偏向。立马把当前线程ID记录到了这个Mark Word当中,修改为偏向(1)。也就是下面这样:

如果线程A,再次进入临界区时 , 判断线程id 是当前自己,就直接执行。也就是说,如果一直没有其它线程过来,线程A就跟没加synchronized一样执行,效率极高。这就是 偏向锁

轻量级锁

当有其它 线程B 也进入临界区了(线程A还没出临界区),就CAS自旋等待很短的时间,如果线程A此时恰好退出临界区了,CAS退出就升级为轻量级锁,否则就是重量级锁。 轻量级锁会构造一个Lock Record锁记录,如下图:

Lock Record 锁记录

Lock Record是线程私有的数据结构,每一个线程都有一个可用Lock Record列表,同时还有一个全局的可用列表。每一个被锁住的对象Mark Word都会和一个Lock Record关联(对象头的MarkWord中的Lock Word指向Lock Record的起始地址),同时Lock Record中有一个Owner字段存放拥有该锁的线程的唯一标识(或者 object mark word ),表示该锁被这个线程占用。如下图所示为Lock Record的内部结构:

轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,必然就会导致轻量级锁膨胀为重量级锁

重量级锁

Synchronized是通过对象内部的一个叫做 监视器锁(Monitor)来实现的 但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间 ,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”

重量级锁的Mark Word结构图:

这个重量级锁的指针指向的就是 ObjectMonitor

//结构体如下
ObjectMonitor::ObjectMonitor() {  
  _header       = NULL;  
  _count       = 0;       //用来记录该线程获取锁的次数
  _waiters      = 0,  
  _recursions   = 0;       //线程的重入次数
  _object       = NULL;  
  _owner        = NULL;    //标识拥有该monitor的线程
  _WaitSet      = NULL;    //等待线程组成的双向循环链表,_WaitSet是第一个节点
  _WaitSetLock  = 0 ;  
  _Responsible  = NULL ;  
  _succ         = NULL ;  
  _cxq          = NULL ;    //多线程竞争锁进入时的单向链表
  FreeNext      = NULL ;  
  _EntryList    = NULL ;    //_owner从该双向循环链表中唤���线程结点,_EntryList是第一个节点
  _SpinFreq     = 0 ;  
  _SpinClock    = 0 ;  
  OwnerIsThread = 0 ;  
} 
  • _owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。
  • cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链表)。cxq是一个临界资源,JVM通过CAS原子指令来修改_cxq队列。
  • EntryList:当线程释放锁时,会将 cxq队列移动到 EntryList, cxq队列中有资格成为候选资源的线程会被移动到该队列中。
  • WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中。

当第一个线程获取到锁时( 重量级锁流程 )

  • 通过CAS尝试把monitor的owner字段设置为当前线程。
  • 如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行
    recursions ++ ,记录重入的次数。
  • 如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获得锁并返回。

第二个线程拿锁竞争时( 重量级锁流程 )

  • 当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ( jstack看是BLOCKED状态 )。
  • 通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点push到_cxq列表中。
  • node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待被唤醒。

第一个线程释放锁时( 重量级锁流程 )

  • 退出同步代码块时会让_recursions减1,当_recursions的值减为0时,说明线程释放了锁。
  • 根据不同的策略(由QMode指定),首先从_EntryList取出唤醒ObjectWaiter对象的线程,如果_EntryList的首元素为空,就取cxq的首元素,放入_EntryList,然后再从_EntryList中取出,唤醒操作最终由unpark完成。
  • 被唤醒的线程,继续执行monitor的竞争。

执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。

锁升级实验

教你分析对象头结构

上面讲的都是在32位机器上的对象头结构,我的机器是64。所以找了个64位对象头结构:

我们得知 64位的JVM Mark Word 共占了 64字节

有这么一段程序,打印对象信息:

#######先引入maven
<dependency>
		<groupId>org.openjdk.jol</groupId>
		<artifactId>jol-core</artifactId>
		<version>0.8</version>
</dependency>    

public class Server {
    private String hadluo = "1234";
    public static void main(String[] args) {
        Server lock = new Server();
        System.err.println(lock);
        System.err.println(ClassLayout.parseInstance(lock).toPrintable());
    }
}

打印结果

arithmetic.Server@15db9742
arithmetic.Server object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header) Mark Word                          01 42 97 db (00000001 01000010 10010111 11011011) (-610844159)
      4     4                    (object header) Mark Word                          15 00 00 00 (00010101 00000000 00000000 00000000) (21)
      8     4                    (object header) Klass pointer                          05 c0 00 f8 (00000101 11000000 00000000 11111000) (-134168571)
     12     4   java.lang.String Server.hadluo                             (object)
Instance size: 16 bytes       // 实例数据
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total  //对齐填充

第一行加第二行是8*8=64bit ,所以第一行和第二行加起来就是Mark Word 数据。数据为(大小端存储,倒过来):

00000000 00000000 00000000 00010101 11011011 10010111 01000010 00000001

分析如下:

锁标记为01 ,偏向为0(无偏向) : 代表这是初始的对象头状态。

hashcode为: 0010101110110111001011101000010 , 转换成16进制为 15DB9742

https://pic1.zhimg.com/v2-7641bcce1dfa3562838a2c2424dd4790_b.jpg

正好是我们打印的toString的值:

arithmetic.Server@15db9742

偏向锁怎么偏向某线程的?

public class Server {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        Object object = new Object();
        System.out.println("main:" + ClassLayout.parseInstance(object).toPrintable());
        new Thread() {
            public void run() {
                synchronized (object) {
                    System.out.println("thread1:" + ClassLayout.parseInstance(object).toPrintable());
                }
            };
        }.start();
    }
}


main:java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

thread1:java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 d0 95 1f (00000101 11010000 10010101 00011111) (529911813)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)

第一个Mark Word (sleep5秒让jvm加载了偏向)

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101

锁标记为01 代表是偏向锁且正处于偏向状态 1
但是偏向的线程id为0表示现在正处于要偏向某线程的状态


第二个Mark Word (进入synchronized)

00000000 00000000 00000000 00000000 00011111 10010101 11010000 00000101

锁标记为01 代表是偏向锁且正处于偏向状态 1
偏向线程id位有值代表已经偏向了某线程

总结

初始化时,是待偏向状态,当第一次进入synchronized 时就处于偏向某线程的状态。

怎么升级到轻量级锁?

当多个线程交替进入临界区时,就会变成轻量级锁。

public class Server {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        Object object = new Object();
        // 竞争object 的线程 ,会一直自旋等待
        new Thread() {
            public void run() {
                synchronized (object) {
                }
                System.out.println("释放锁后的状态:" + ClassLayout.parseInstance(object).toPrintable());
            };
        }.start();      
        Thread.sleep(3000);
        new Thread() {
            public void run() {
                synchronized (object) {
                    System.out.println("交替另一个线程进来状态:" + ClassLayout.parseInstance(object).toPrintable());
                }
            };
        }.start();
    }
}

打印结果如下

释放锁后的状态:java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 78 64 1e (00000101 01111000 01100100 00011110) (509900805)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

交替另一个线程进来状态:java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           88 f5 18 20 (10001000 11110101 00011000 00100000) (538506632)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

现象总结

  • 先让一个线程A拿锁,然后又释放锁,此时是偏向某线程状态。
  • 线程B在执行(线程A已经释放,没有竞争,只是交替拿锁),发现锁标志为00 就升级为轻量级锁了。

怎么升级到量级锁?

多个线程同时进入临界区时,就会升级为重量级锁。

public class Server {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        Object object = new Object();
        // 竞争object 的线程 ,会一直自旋等待
        new Thread() {
            public void run() {
                synchronized (object) {
                    System.err.println("线程A拿到锁,进入临界区");
                    // 不释放锁 ,等10秒。 其它线程来竞争
                    try {
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("有线程来竞争后的状态:" + ClassLayout.parseInstance(object).toPrintable());
                }
            };
        }.start();      
        Thread.sleep(3000);
        new Thread() {
            public void run() {
                System.err.println("线程B开始竞争");
                synchronized (object) {
                }
            };
        }.start();
    }
}

打印结果

线程A拿到锁,进入临界区
线程B开始竞争
有线程来竞争后的状态:java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           6a e6 ae 1c (01101010 11100110 10101110 00011100) (481224298)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

现象总结

  • 线程A先拿到锁进入等待,此时是偏向线程A状态,然后线程A睡眠。
  • 然后线程B进入临界区拿锁,发现是偏向线程A的,于是升级为重量级锁。
  • 线程A睡眠完成,打印就变成了重量级锁了。


强烈推荐一个 进阶 JAVA架构师 的博客

https://pic2.zhimg.com/v2-1e8deea0c94dab83067a8eca4007734d_ipico.jpg

支付宝打赏 微信打赏

如果文章对您有帮助,您可以鼓励一下作者