Synchronized同步锁实现原理

修饰代码块

    // 关键字在代码块上,锁为括号里面的对象
    public void method2() {
        Object o = new Object();
        synchronized (o) {
            // code
        }
    }
复制代码

Synchronized 在修饰同步代码块时,是由 monitorenter 和 monitorexit 指令来实现同步的。进入 monitorenter 指令后,线程将持有 Monitor 对象,退出 monitorenter 指令后,线程将释放该 Monitor 对象。

  // access flags 0x1
  public method2()V
    TRYCATCHBLOCK L0 L1 L2 null
    TRYCATCHBLOCK L2 L3 L2 null
   L4
    LINENUMBER 16 L4
    NEW java/lang/Object
    DUP
    INVOKESPECIAL java/lang/Object.<init> ()V
    ASTORE 1
   L5
    LINENUMBER 17 L5
    ALOAD 1
    DUP
    ASTORE 2
    MONITORENTER
   L0
    LINENUMBER 19 L0
    ALOAD 2
    MONITOREXIT
   L1
    GOTO L6
   L2
   FRAME FULL [com/dragon/learn/leean1/SynchronizedTest java/lang/Object java/lang/Object] [java/lang/Throwable]
    ASTORE 3
    ALOAD 2
    MONITOREXIT
   L3
    ALOAD 3
    ATHROW
   L6
    LINENUMBER 20 L6
   FRAME CHOP 1
    RETURN
   L7
    LOCALVARIABLE this Lcom/dragon/learn/leean1/SynchronizedTest; L4 L7 0
    LOCALVARIABLE o Ljava/lang/Object; L5 L7 1
    MAXSTACK = 2
    MAXLOCALS = 4
}
复制代码

修饰方法

当 Synchronized 修饰同步方法时,并没有发现 monitorenter 和 monitorexit 指令,而是出现了一个 ACC_SYNCHRONIZED 标志。

Monitor

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

复制代码

当多个线程同时访问同一个代码块时,首先会将这先线程放入ContenionList和EntryList中。之后线程通过操作系统的Mutex Lock来获取锁。如果获取到了,则执行相应的代码。如果没有获取到,则重新进入ContenionList。如果调用了wait方法,则会进入WaitSet。当其他线程调用notify方法时会唤醒并重新进入EntryList.

锁升级优化

Java对象头

Java对象有对象头,实例数据,填充数据三部分组成。其中对象头由标记字段,类型指针,数组长度三部分组成。

偏向锁

偏向锁主要是用来优化同一个线程多次申请同一个锁的竞争。偏向锁的作用时当一个线程再次访问同步代码或方法时,只需在对象头上判断线程的偏向锁的线程ID是否为当前线程。如果是的话,则不用再次进入Monitor去竞争对象了。

如果有其他线程竞争该资源时,则改偏向锁就会被撤消。偏向锁的撤销需要等待全局安全点,暂停持有该锁的线程。同时检查该线程是否还在执行该方法,如果是,则升级锁,反之,则其他线程抢占。

扫描二维码关注公众号,回复: 7644716 查看本文章

因此,在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁就会被撤销,发生 stop the word 后, 开启偏向锁无疑会带来更大的性能开销,这时我们可以通过添加 JVM 参数关闭偏向锁来调优系统性能,示例代码如下:

偏向锁设置方法


-XX:-UseBiasedLocking //关闭偏向锁(默认打开)

-XX:+UseHeavyMonitors  //设置重量级锁
复制代码

轻量级锁

当另外有一个线程获取锁时,发现该锁已经是偏向锁了,那么就会通过CAS的方式去获取锁,如果获取成功,那么直接替换标记字段的类型线程ID为当前线程。如果获取失败,那么就会撤偏向锁,转为轻量级锁。

轻量级锁适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在长时间的竞争。

自旋锁和重量级锁

轻量级锁CAS获取锁失败, 默认会通过自旋的方式来获取锁。自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。

锁消除与锁粗化

JIT编译器在动态编译同步代码块的时候,会通过逃逸分析的技术。如果确定这个代码块只会被一个线程访问,那么就会进行锁消除。

锁粗化同理,就是在 JIT 编译器动态编译时,如果发现几个相邻的同步块使用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁“所带来的性能开销。

减小锁粒度

这个主要是在代码层面进行优化。

例如,JDK8之前的ConcurrentHashMap,通过分段的机制来控制。

总结

  1. 检测Mark Word里面是不是当前线程ID,如果是,表示当前线程处于偏向锁
  2. 如果不是,则使用CAS将当前线程ID替换到Mark Word,如果成功则表示当前线程获得偏向锁,设置偏向标志位1
  3. 如果失败,则说明发生了竞争,撤销偏向锁,升级为轻量级锁
  4. 当前线程使用CAS将对象头的mark Word锁标记位替换为锁记录指针,如果成功,当前线程获得锁
  5. 如果失败,表示其他线程竞争锁,当前线程尝试通过自旋获取锁 for(;;)
  6. 如果自旋成功则依然处于轻量级状态
  7. 如果自旋失败,升级为重量级锁

猜你喜欢

转载自juejin.im/post/5dac75dce51d45782a479154