Timetombs

泛义的工具是文明的基础,而确指的工具却是愚人的器物

66h / 116a
,更新于 2024-04-06T22:06:23Z+08:00 by   35f4f88

[Java] synchronized

版权声明 - CC BY-NC-SA 4.0

在Java中,有一个与锁有关的关键字synchronized,它是由JVM层面提供的Monitor来实现。

1. 语法

synchronized关键字在语法层面1有两种形式。请看如下的示例代码:

import java.io.File;

public class SynchronizedExample {

  public void instanceSynchronizedStatementMethod(Object syncObject) {
    synchronized (syncObject) {
      instanceMethod();
    }
  }

  public synchronized void synchronizedInstanceMethod() {
    instanceMethod();
  }

  public void instanceMethod() {
  }

  public static synchronized void synchronizedStaticMethod() {
    staticMethod();
  }

  public static void staticMethod() {
  }
}

1.1 修饰代码快

示例中的instanceSynchronizedStatementMethod方法,这种形式的语法要求synchronized关键字关联一个对象作为同步资源,然后紧跟着是一个代码块。可以保证在一个JVM进程内,多个使用syncObject作为同步资源对象的线程同时访问这个代码块的时候,只能串行的进行访问,也就是同一时刻,只会有一个线程成功的进入这个代码块执行代码。当然这个同步资源对象syncObject也可以是一个字段,实例的或者静态的都可以,包裹这个同步代码块的方法同样的也是实例的或者静态的都可以。

这种语法形式的关键在于多个线程持有的syncObject是不是同一个,同一个的为同一个阻塞队列。

由此就可以推导出来,Java中的intbytechar等等这些基本的原始类型是不能作为syncObject的,因为它们直接存储的是值,而不是引用;其次Integer这种包装类型由于存在装箱拆箱,也是不可以的;再次String由于有字符串驻留池的存在,也无法确保syncObject不会出现错乱。

故而最好的方式就是new Object()即可,当然你也不能有10个线程,每个线程都new一个自己的syncObject,而是让需要同步的那些个线程使用同一个syncObject即可。

当你需要在整个JVM内同步所有线程时,选一个JVM内单例的syncObject即可,比如一个静态的字段,或者一个类型的class对象。

1.2 修饰方法

示例中的synchronizedInstanceMethodsynchronizedStaticMethod都是方法级别的语法。其差异在于前者是锁的this对象,后者锁的是SynchronizedExample.class这个对象。由于后者在一个JVM进程内是唯一的,故而相当于会影响所有的访问这个方法的线程。

2 字节码

通过字节码来看一下编译后的代码。

public class SynchronizedExample
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#22         // java/lang/Object."<init>":()V
   #2 = Methodref          #4.#23         // SynchronizedExample.instanceMethod:()V
   #3 = Methodref          #4.#24         // SynchronizedExample.staticMethod:()V
   #4 = Class              #25            // SynchronizedExample
   #5 = Class              #26            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               instanceSynchronizedStatementMethod
  #11 = Utf8               (Ljava/lang/Object;)V
  #12 = Utf8               StackMapTable
  #13 = Class              #25            // SynchronizedExample
  #14 = Class              #26            // java/lang/Object
  #15 = Class              #27            // java/lang/Throwable
  #16 = Utf8               synchronizedInstanceMethod
  #17 = Utf8               instanceMethod
  #18 = Utf8               synchronizedStaticMethod
  #19 = Utf8               staticMethod
  #20 = Utf8               SourceFile
  #21 = Utf8               SynchronizedExample.java
  #22 = NameAndType        #6:#7          // "<init>":()V
  #23 = NameAndType        #17:#7         // instanceMethod:()V
  #24 = NameAndType        #19:#7         // staticMethod:()V
  #25 = Utf8               SynchronizedExample
  #26 = Utf8               java/lang/Object
  #27 = Utf8               java/lang/Throwable
{
  public SynchronizedExample();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public void instanceSynchronizedStatementMethod(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: aload_1
         1: dup
         2: astore_2
         3: monitorenter
         4: aload_0
         5: invokevirtual #2                  // Method instanceMethod:()V
         8: aload_2
         9: monitorexit
        10: goto          18
        13: astore_3
        14: aload_2
        15: monitorexit
        16: aload_3
        17: athrow
        18: return
      Exception table:
         from    to  target type
             4    10    13   any
            13    16    13   any
      LineNumberTable:
        line 6: 0
        line 7: 4
        line 8: 8
        line 9: 18
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 13
          locals = [ class SynchronizedExample, class java/lang/Object, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public synchronized void synchronizedInstanceMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #2                  // Method instanceMethod:()V
         4: return
      LineNumberTable:
        line 12: 0
        line 13: 4

  public void instanceMethod();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 16: 0

  public static synchronized void synchronizedStaticMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: invokestatic  #3                  // Method staticMethod:()V
         3: return
      LineNumberTable:
        line 19: 0
        line 20: 3

  public static void staticMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 23: 0
}
SourceFile: "SynchronizedExample.java"

可以看出instanceSynchronizedStatementMethodinstanceMethod中多来很多的指令,主要是monitorentermonitorexit这两个,前者代表加锁,后者代表释放锁,由于不知掉内部会不会抛出异常,故而编译器自动添加来finaly块来保证锁的释放。

synchronizedInstanceMethodsynchronizedStaticMethod两个方法就比较简单了,只是增加来一个标记ACC_SYNCHRONIZED。当JVM遇见这个标记的方法时,会使用和上面一样的monitorentermonitorexit一样的方式来执行加锁和解锁行为。

3 实现原理

JVM底层是依赖Java的对象头中的Mark WordMonitor来实现的synchronized

3.1 Mark Word

锁的四种状态体现在下面的表格中。

StateMark Word (32 bit)
Octet 1Octet 2Octet 3Octet 4
12345678123456781234567812345678
23 bit2 bit4 bitIs Biased LockFlag
unlockedidentity hashcodeage001
biased lockthread idepochage101
lightweight lockpointer to stack lock record00
heavyweight lockpointer to monitor10
marked for gc11

3.2 Monitor

我们通过上述的字节码已经得知了monitorentermonitorexit指令以及附加到方法上的ACC_SYNCHRONIZED标记。在文章开头提到synchronized是由JVM层面提供的Monitor来实现,那么这些指令和标记就是为Monitor而准备的,在JVM中通过C++实现的ObjectMonitor2来提供支持。

java.lang.ObjectwaitnotifynotifyAll这些native方法也是由ObjectMonitor实现的。

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
}

这里由几个重要的字段:

  1. _owner:拥有当前对象的线程地址。
  2. _WaitSet:存放调用wait方法后进入等待状态的线程的队列。
  3. _EntryList:等待锁block状态的线程的队列。
  4. _recursions:锁的重入次数。
  5. _count:线程获取锁的次数。

3.3 Heavyweight Lock

Jdk1.6之前,synchronized在JVM底层就只是传统意义上的依赖OS内核mutex结合Mark WordMonitor实现的传统意义上的锁,现在称之为重量级锁。

在上面的Mark Word表格中,当Flag10时,代表前面的30bit是指向Monitor对象的指针。

3.4 Lightweight Lock

在Jdk1.6时,引入了轻量级锁。自旋锁,自适应锁。

3.5 Biased Lock

在Jdk1.6时,引入了偏向锁。

4 遗留问题

使用synchronized来进行同步是非常方便的,JVM也在进行持续的优化,性能也可以得到满足。但是因为这一切都是JVM内部实现的,有些个别的需求它依然无法满足。

  1. 有些情况下效率达不到要求。
  2. 获取锁的状态。
  3. 不可中断。
  4. 不够灵活。

5 参考资料

《Eliminating Synchronization-Related Atomic Operations withBiased Locking and Bulk Rebiasing》 : https://www.oracle.com/technetwork/java/biasedlocking-oopsla2006-wp-149958.pdf

Synchronization and Object Locking : https://wiki.openjdk.java.net/display/HotSpot/Synchronization

上一篇 : [Java] CAS(Compare And Swap)