美文网首页
Synchronized优化原理

Synchronized优化原理

作者: 林嘻嘻呀 | 来源:发表于2020-05-25 16:08 被阅读0次

synchronized:俗称对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

synchronized 实际是用对象锁保证了临界区内代码的==原子性==,临界区内的代码对外是不可分割的,不会被线程切换所打断。

整体锁状态升级流程如下:

  • image.png

Java的对象头

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

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

  1. 第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,称它 为“Mark Word”,Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间。

  2. 对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针

    来确定该对象是哪个类的实例。此外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据

以32位虚拟机为例:

  1. 对象头格式
  2. 数组对象头格式
  3. Mark Word格式
  • 无锁状态
  • 偏向锁状态
  • 轻量级锁状态
  • 重量级锁状态
  • GC标记
  1. 64位虚拟机下
  • image.png

Monitor

Monitor 被翻译为监视器管程

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向 Monitor 对象的指针
在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.cpp文件,C++实现的)

ObjectMonitor() {
   _header       = NULL;
   _count        = 0; 
   _waiters      = 0,
   _recursions   = 0;
   _object       = NULL;
   _owner        = NULL;  //持有者
   _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
   _WaitSetLock  = 0 ;
   _Responsible  = NULL ;
   _succ         = NULL ;
   _cxq          = NULL ;
   FreeNext      = NULL ;
   _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
   _SpinFreq     = 0 ;
   _SpinClock    = 0 ;
   OwnerIsThread = 0 ;
 }

Monitor简图:

  • Monitor
  • 临界区代码
synchronized(obj){
    .......
}

具体流程:

  1. 最开始WaitSetEntryListOwner都是NULL
  2. 假设有Thread-2想要执行上面代码,回去检查obj对象是否与monitor有关联,发现没有,就会将Monitor的所有者Owner置为 Thread-2Monitor中只能有一个Owner
  3. 如果Thread-3Thread-4Thread-5也来执行synchronized(obj),依旧先检查obj是否关联Monitor,发现关联了然后检查看看Monitorowner是否有关联,有就会进入EntryList阻塞
  4. Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁 ,执行哪一个线程由 CPU 来调度
  5. 图中 WaitSet 中的 Thread-0Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程

\color{ Red } { synchronized 必须是进入同一个对象的 monitor 才有上述的效果 }


synchronized 优化原理

synchronized的实现依赖于与某个对象向关联的monitor实现,而monitor是基于底层操作系统的Mutex Lock实现的,而基于Mutex Lock实现的同步必须经历从用户态到核心态的转换,这个开销特别大,成本非常高。所以频繁的通过synchronized实现同步会严重影响到程序效率,而这种依赖于Mutex Lock实现的锁机制也被称为重量级锁,为了减少重量级锁带来的性能开销,JDK对synchronized进行了种种优化。

1. 轻量级锁

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

static final Object obj = new Object();
public static void method1() {
 synchronized( obj ) {
 // 同步块 A
 method2();
 }
}
public static void method2() {
 synchronized( obj ) {
 // 同步块 B
 }
}
  1. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”)创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构(不可见),内部可以存储锁定对象的Mark Word,然后拷贝对象头中的Mark Word复制到锁记录中。

    image.png
  2. 拷贝成功后,JVM将使用 cas 操作尝试将对象的Mark Word更新为指向Lock Record的指针,并让锁记录中 Object reference 指向锁对象

    image.png
    • 如果 cas 替换成功,表示由该线程给对象加锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。如下图
      image.png
  • 如果 cas 失败,有两种情况
    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是同一线程执行了 synchronized操作, 锁重入,那么再添加一条 Lock Record 作为重入的计数(这种情况建立在是可重入锁的情况)
      • 按照我们上面的代码,调用method2()会再一次进行加锁操作,此时 cas 操作肯定会失败,因为object里面已经是轻量级锁状态,但是通过object里面的 lock record地址,会发现是同一个线程,此时操作如下图,锁记录为null说明存在锁重入,然后继续执行后面代码,有几个null值重入几次,
        image.png
  1. 轻量级锁解锁
  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象
    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
2. 锁膨胀

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

image.png
  • Thread-1 加轻量级锁失败,进入锁膨胀流程
    • Object对象申请Monitor 锁,让 Object 指向重量级锁地址
    • 自己进入 MonitorEntryList BLOCKED
    • image.png
    • Thread-0退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Ownernull,唤醒 EntryListBLOCKED 线程
3. 自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞,一般默认是10次
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

4. 偏向锁

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

以上基本来源于B站学习并发编程的笔记地址

相关文章

网友评论

      本文标题:Synchronized优化原理

      本文链接:https://www.haomeiwen.com/subject/buttahtx.html