Java Thread系列(五)synchronized

南笙酒味 提交于 2020-03-01 03:14:28

Java Thread系列(五)synchronized

本文我们讨论 synchronized 重量级锁的实现原理。

一、synchronized 实现原理

1.1 synchronized 修饰符对应的字节码指令

我们知道在 java中synchronized 主要有两种使用形式:同步方法和同步代码块。 synchronized 可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。Java 中每一个对象都可以作为锁,这是 synchronized 实现同步的基础:

(1)普通同步方法,锁是当前实例对象
(2)静态同步方法,锁是当前类的class对象
(3)同步方法块,锁是括号里面的对象接下来

public class SynchronizedTest {

    // 作用于类级别
    public synchronized static void testClass() {
        System.out.println("synchronized testClass!!!");
    }

    // 作用于方法级别
    public synchronized void testMethod() {
        System.out.println("synchronized testMethod!!!");
    }

    // 作用于代码块级别
    public void testBlock() {
        synchronized (this) {
            System.out.println("synchronized testBlock!!!");
        }
    }
}

通过 javap -v SynchronizedTest.class 反编译后的代码如下:

(1) 类级别

public static synchronized void testClass();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED

(2) 方法级别

public synchronized void testMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED

(3) 代码块级别

 public void testBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String synchronized test!!!
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
        22: return

从 SynchronizedTest 反编译后的代码可以看到:

  1. synchronized 方法反编译后,输出的字节码有 ACC_SYNCHRONIZED 标识
  2. synchronized 代码块反编译后,输出的字节码有 monitorenter 和 monitorexit 语句

由此我们可以猜测

  1. synchronized 的作用域不同,JVM 底层实现原理也不同
  2. synchronized 代码块是通过 monitorenter 和 monitorexit 来实现其语义的
  3. synchronized 方法是通过 ACC_SYNCRHONIZED 来实现其语义的

那么虚拟机字节码引擎是怎么解读 monitorenter/monitorexit 和 ACC_SYNCRHONIZED 的?下一部分我们就介绍这个。

1.2 monitorenter/monitorexit 实现原理

我们先看一下 JVM 规范是怎么定义 monitorenter 和 monitorexit 的👇👇👇

(1) monitorenter:

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

翻译过来:

每一个对象都会和一个监视器 monitor 关联。监视器被占用时会被锁住,其他线程无法来获取该 monitor。
当 JVM 执行某个线程的某个方法内部的 monitorenter 时,它会尝试去获取当前对象对应的 monitor 的所有权。其过程如下:

  1. 若 monior 的进入数为 0,线程可以进入 monitor,并将 monitor 的进入数置为 1。当前线程成为 monitor 的 owner(所有者)
  2. 若线程已拥有 monitor 的所有权,允许它重入 monitor,并递增 monitor 的进入数
  3. 若其他线程已经占有 monitor 的所有权,那么当前尝试获取 monitor 的所有权的线程会被阻塞,直到 monitor 的进入数变为 0,才能重新尝试获取 monitor 的所有权。

(2) monitorexit:

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

翻译过来:

  1. 能执行 monitorexit 指令的线程一定是拥有当前对象的 monitor 的所有权的线程。
  2. 执行 monitorexi 时会将 monitor 的进入数减 1。当 monitor 的进入数减为 0 时,当前线程退出 monitor,不再拥有 monitor 的所有权,此时其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权。

1.3 ACC_SYNCRHONIZED实现原理

当 JVM 执行引擎执行某一个方法时,其会从方法区中获取该方法的 access_flags,检查其是否有 ACC_SYNCRHONIZED 标识符,若是有该标识符,则说明当前方法是同步方法,需要先获取当前对象的 monitor,再来执行方法。

二、synchronized 底层语义原理

Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现,无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。下面先来了解一个概念 Java 对象头,这对深入理解 synchronized 实现原理非常关键。

2.1 理解 Java 对象头与 Monitor

HotSpot 虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下:

Object 在 JVM 中的结构

  • 对象头:包括 Mark Word 和类型指针两部分。
  • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按 4 字节对齐。
  • 填充数据:由于虚拟机要求对象起始地址必须是 8 字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,了解即可。

2.1.1 对象头

对象头包括两部分:Mark Word 和 类型指针。

(1) Mark Word

Mark Word 里默认的存放的对象的 Hashcode,分代年龄和锁标记位 ,它是实现 synchronized 的锁对象的基础。(摘自《java并发编程的艺术》)。32 位 JVM 的 Mark Word 默认存储结构如下:

Mark Word存储结构

Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。对象的 MarkWord 变化为下图:

Mark Word状态变化
如图在 Mark Word 会默认存放 hasdcode,年龄值以及锁标志位等信息。

(2) 类型指针

类型指针指向对象的类元数据,虚拟机通过这个指针确定该对象是哪个类的实例。

2.2 monitor 神秘面纱

2.2.1 Object 对象在 JVM 中的实现

在 Java 虚拟机(HotSpot)中,monitor 是由 ObjectMonitor 实现的。oop.hpp 中定义了 Java Object 对象在 JVM 中的结构 oopDesc,每个 Java Object 对象都包含一个 markOopDesc,这个类会通过 monitor 方法创建一个 ObjectMonitor 对象。

// oop.hpp
// Object -> oop/oopDesc -> ObjectMonitor -> markOopDesc(对象头 Mark Word)
class oopDesc {
    volatile markOop  _mark;
}

2.2.2 Mark Word

HotSpot 通过 markOop 类型实现 Mark Word,具体实现位于 markOop.hpp 文件中。
由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,markOop 被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间,32 位虚拟机的 markOop 实现如下:

//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
  • hash: 保存对象的哈希码
  • age: 保存对象的分代年龄
  • biased_lock: 偏向锁标识位
  • lock: 锁状态标识位
  • JavaThread:* 保存持有偏向锁的线程ID
  • epoch: 保存偏向时间戳

在 markOopDesc 可以通过 monitor 方法创建了一个 ObjectMonitor 对象。

// markOop.hpp
// markOopDesc 继承自 oopDesc 类,monitor 方法创建了一个 ObjectMonitor 对象。
class markOopDesc: public oopDesc {

    ObjectMonitor* monitor() const {
    assert(has_monitor(), "check");
    // Use xor instead of &~ to provide one extra tag-bit check.
    return (ObjectMonitor*) (value() ^ monitor_value);
  }
}

2.2.3 ObjectMonitor

在 hotSpot 虚拟机中,monitor 是由 ObjectMonitor 实现的。其源码是用 c++ 来实现的,位于 objectMonitor.hpp 文件中。ObjectMonitor 主要数据结构如下:

// objectMonitor.hpp
ObjectMonitor() {
    _header       = NULL;   // markOop 对象头
    _count        = 0;      // 用来记录该线程获取锁的次数 
    _waiters      = 0,      // 等待线程数
    _recursions   = 0;      // 重入次数
    _object       = NULL;
    _owner        = NULL;   // 指向获取 ObjectMonitor 对象的线程
    _WaitSet      = NULL;   // 调用 wait 方法后的线程会加入到 _WaitSet 中
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;  // 尝试进入 synchronized 代码块的线程会封装成 ObjectWaiter 并加入到 _cxq 中
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 处于等待锁 block 状态的线程
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;// 监视器前一个拥有者的线程 id
}

① _owner:初始时为 NULL。当有线程占有该 monitor 时,owner 标记为该线程的唯一标识。当线程释放 monitor 时,owner 又恢复为 NULL。owner 是一个临界资源,JVM 是通过 CAS 操作来保证其线程安全的。
② _cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq 是一个后进先出的 stack(栈)。
③ _EntryList:_cxq 队列中有资格成为候选资源的线程会被移动到该队列中。_cxq/_EntryList 相当于 AQS 中的 sync queue
④ _WaitSet:因为调用 wait 方法而被阻塞的线程会被放在该队列中。_WaitSet 相当于 AQS 中的 condition queue
⑤ _recursions:锁的重入次数 。
⑥ _count:用来记录该线程获取锁的次数。

当多个线程同时访问一段同步代码时,首先会进入 _EntryList(_cxq) 队列中,当某个线程获取到对象的 monitor 后进入 _Owner 区域并把 monitor 中的 _owner 变量设置为当前线程,同时 monitor 中的计数器 _count 加 1。即获得对象锁。
若持有 monitor 的线程调用 wait() 方法,将释放当前持有的 monitor,_owner 变量恢复为 null,_count 自减1,同时该线程进入 _WaitSet 集合中等待被唤醒。若当前线程执行完毕也将释放 monitor(锁)并复位变量的值,以便其他线程进入获取 monitor(锁)。如下图所示:

EntryList和WaitSet

2.3 synchronized 在 JVM 在的实现

JVM源码分析之synchronized实现:https://www.jianshu.com/p/c5058b6fe8e5


每天用心记录一点点。内容也许不重要,但习惯很重要!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!