并发 - synchronized

synchronized

简介

Java Monitor 支持两种线程同步:

  • mutual exclusion:通过来支持
  • cooperation:通过 Object.wait()Object.notify() 来支持。

锁的对象是谁

当你使用 synchronized 关键字的时候,JVM 底层使用 Monitor 锁来实现同步。而锁的对象可以分为:

  • 如果 synchronized 的是普通方法,那么锁是当前实例
  • 如果 synchronized 的是静态方法,那么锁是当前类的 Class
  • 如果 synchronized 的是同步块,那么锁是括号里面的对象

synchronized 同步块

底层基于 monitorentermonitorexit 这一对指令实现的。

public void foo(Object lock) {
    synchronized (lock) {
        lock.hashCode();
    }
}

上面的 Java 代码将编译为下面的字节码:

public void foo(java.lang.Object);
Code:
    0: aload_1
    1: dup
    2: astore_2
    3: monitorenter
    4: aload_1
    5: invokevirtual java/lang/Object.hashCode:()I
    8: pop
    9: aload_2
    10: monitorexit
    11: goto          19
    14: astore_3
    15: aload_2
    16: monitorexit
    17: aload_3
    18: athrow
    19: return
Exception table:
    from    to  target type
        4    11    14   any
        14    17    14   any

synchronized 方法

方法标记为 ACC_SYNCHRONIZED,同样需要进行 monitorenter 操作。

public synchronized void foo(Object lock) {
    lock.hashCode();
}

上面的 Java 代码将编译为下面的字节码:

public synchronized void foo(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
    stack=1, locals=2, args_size=2
        0: aload_1
        1: invokevirtual java/lang/Object.hashCode:()I
        4: pop
        5: return

synchronized 底层实现

JVM 中的同步是基于进入和退出管程(Monitor)对象实现的。每个对象实例都会有一个 Monitor,Monitor 可以和对象一起创建、销毁。Monitor 是由 ObjectMonitor 实现,而 ObjectMonitor 是由 C++ 的 ObjectMonitor.hpp 文件实现,如下所示:

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 ;
}

当多个线程同时访问一段同步代码时,多个线程会先被存放在 ContentionList_EntryList 集合中,处于 block 状态的线程,都会被加入到该列表。接下来当线程获取到对象的 Monitor 时,Monitor 是依靠底层操作系统的 Mutex Lock 来实现互斥的,线程申请 Mutex 成功,则持有该 Mutex,其它线程将无法获取到该 Mutex,竞争失败的线程会再次进入 ContentionList 被挂起。

如果线程调用 wait() 方法,就会释放当前持有的 Mutex,并且该线程会进入 WaitSet 集合中,等待下一次被唤醒。如果当前线程顺利执行完方法,也将释放 Mutex。

如下是 Thread Synchronization 介绍的:

monitor region 就是一段代码

新来的线程首先进入 entry set 队列等待,当当前占有 monitor 的线程退出后,这个新来的线程必须与其它在 entry set 队列中的线程一起竞争。当线程执行 wait 的时候,这个线程释放 monitor,然后进入 wait set 队列,除非位于 monitor 中的另外一个线程 notify 这个线程,否则这个线程将一直处于 suspended 状态。

当位于 monitor 中的线程 notify 后,它将继续持有这个 monitor,除非它自己再调用 wait 或者它自己退出 monitor region 区域。当线程释放 monitor 之后,位于 wait set 中的线程将会恢复并且重新获取 monitor。如果线程退出 monitor 的时候,没有调用任何 notify(),那么只有位于 entry set 中的线程具有资格竞争 monitor,否则 entry setwait set 中的线程一起竞争 monitor

在 JVM 中,wait 命令可以指定一个超时时间。当超时时间到了之后,这个线程可以自己从 wait set 中恢复过来。

synzhronized 性能改进

在 Java 6 之前,Monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作

现代的(Oracle)JDK 中,JVM 对此进行了非常大地改进,提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)轻量级锁重量级锁,大大改进了其性能。

所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。

对象头

对于 HotSpot JVM,Java 对象保存在内存中时,由对象头、实例数据、对齐填充字节组成。对象头由 **Mark Word、指向类的指针、数组长度(只有数组对象才有)**组成。

Mark Word 记录了对象和锁有关的信息,当这个对象被 synchronized 关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word 有关。Mark Word 在 32 位 JVM 中的长度是 32 bit,在 64 位 JVM 中长度是 64 bit。Mark Word 在不同的锁状态下存储的内容不同,在 32 位JVM中是这么存的:

偏斜锁

当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。

轻量级锁

如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

重量级锁

轻量级锁 CAS 获取锁失败,会升级为重量级锁。

锁降级

当 JVM 进入安全点(SafePoint)的时候,会检查是否有闲置的 Monitor,然后试图进行降级。HotSpot JVM 的 Stop-the-World 机制称为 safepoint,在此期间,所有线程(不含 JNI 线程)会被挂起。

可重入与公平性

synchronized 是非公平锁,可以重入。可重入意味着获取锁的操作的粒度是线程,而非调用。如果不可重入,那么下面代码可能会发生死锁:

public class Widget {

    public synchronized void doSomething() {}

}

public class LoggingWidget extends Widget {

    public synchronized void doSomething() {
        System.out.println("called doSomething");
        super.doSomething();
    }

}

synchronized vs Lock