synchronized底层原理解析

Synchronized原理解析

synchronized 对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit

synchronized 关键字底层原理属于 JVM 层面。

1. 编译测试

① synchronized 同步语句块的情况

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代码块");
        }
    }
}

通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行 javap -c -s -v -l SynchronizedDemo.class 反编译。

public class javase.thread.SynchronizedDemo {
  public javase.thread.SynchronizedDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  public void method();
    Code:
       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 代码块
       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
    Exception table:
       from    to  target type
           4    14    17   any
          17    20    17   any
}

从上面我们可以看出:

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor 对象存在于每个 Java 对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么 Java 中任意对象可以作为锁的原因) 的持有权。当计数器为 0 则可以成功获取,获取后将锁计数器设为 1 也就是加 1。相应的在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。如果有可重入的情况,锁计数器会持续增加

② synchronized 修饰方法的的情况

public class SynchronizedDemo2 {
	public synchronized void method() {
		System.out.println("synchronized 方法");
	}
}

同样执行上述操作获取字节码。

{
  public javase.thread.SynchronizedDemo2();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ljavase/thread/SynchronizedDemo2;

  public static synchronized void method();
    descriptor: ()V
    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String synchronized 方法
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
}

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法(注意:要出现这个标识,javap 指令必须加 -v 参数,不然显示不完全),JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

总结:Java 虚拟机中的 synchronized 是基于进入和退出 monitor 对象实现的,同步分为显式同步和隐式同步,同步代码块代表着显式同步,指的是有明确的 monitorenter 和 monitorexit 指令。同步方法代表着隐式同步,同步方法是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。

2. 对象头

在 HotSpot 虚拟机中,对象在内存中存储布局分为 3 块区域对象头(Header)、实例数据(Instance Data)、对齐填充(Padding),这里主要关注对象头。

HotSpot 虚拟机的对象头包括两部分(非数组对象)信息,如下图所示:

  • 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳、对象分代年龄,这部分信息称为**“Mark Word”;Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据自己的状态复用**自己的存储空间。
  • 第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
  • 如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中无法确定数组的大小。

Mark Word 在不同的锁状态下存储的内容不同,在 32 位 JVM 中是这么存的:

synchronized底层原理解析_第1张图片

重量级锁就是 synchronized 锁,锁的标记位为 10,其中指针指向 monitor 对象的起始地址每一个对象都存在一个 monitor 与之相关联,monitor 对象可以与对象一起创建销毁或者当线程试图获取对象锁的时候自动生成,monitor 被线程持有之后就处于锁定的状态。JVM 中 monitor 是有 ObjectMonitor 实现的,其主要的数据结构为:

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

monitor 对象中有内部有两个队列,一个用来保存一个 ObjectWaiter 对象列表(每个等待这个锁的线程都会被封装成为ObjectWaiter对象),owner 指向的是持有持有 monitor 对象的线程,当多个线程同时访问一段代码的时候,首先会进入 _Entry_list 集合,当线程获取到对象的 Monitor 后,会进入 _owner 区域并把 monitor 中的 owner 设置为当前线程,同时 monitor 中的计数器 count 加 1,如果线程调用 wait() 方法,将会释放当前持有的 monitor,owner 设置为 null, count 减 1,同时该线程进入 WaitSet 集合中等待被唤醒,如果当前线程执行完毕也将会释放 monitor 锁,owner 变为null,count 减 1。

整个过程可以参考下图。
synchronized底层原理解析_第2张图片

monitor 对象存在每个 Java 对象的对象头中 (MarkWord 结构中存储了指向 monitor 对象的指针),也就是为什么 notify,notifyall,wait 方法都存在 Object 对象中的原因。

总结 synchronized 修饰方法可以看出,JVM 通过判断 ACC_SYNCHRONIZED 访问标志来判别一个方法是否是同步方法,进而获取 monitor 对象,在早期 synchronized 是重量锁效率低,因为 monitor 是依赖底层操作系统来实现的,而操作系统实现线程之间的转换需要从用户太转换到核心态,这个转换要浪费很多时间,后来从 JVM 层面对 synchronized 有了很大的优化,为了减少获得锁或者是释放锁的性能消耗,引入了轻量级锁和偏向锁,所以现在的 synchronized 还可以。

你可能感兴趣的:(Java,synchronized,java)