synchronized底层实现的原理与锁的膨胀

synchronized底层实现的原理

最近在看面经得时候看到有面试官问了这道题,说一说synchronized底层实现的原理,在网上查阅后分享给大家。
关于这个问题,主要有比较重要的两个概念Monitor监视器锁MarkWord(对象标记)

Monitor

每个对象有一个监视器锁(monitor)。当monitorm被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,通过反编译class文件,我们可以看到线程运行过程如下:

synchronized底层实现的原理与锁的膨胀_第1张图片

monitorenter:尝试进入monitor

  • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
  • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit:退出monitor

  • 执行monitorexit的线程必须是objectref所对应的monitor的所有者。

  • 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

为什么会有两个monitorexit呢?

这个主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)。因此最后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁。

MarkWord(对象标记)

Java对象内存模型
avater
一个Java对象由,对象标记,类型指针,真实数据,内存对齐四部分组成。

对象标记也称Mark Word字段,存储当前对象的一些运行时数据。
类型指针,JVM根据该指针确定该对象是哪个类的实例化对象。
真实数据自然是对象的属性值。
内存补齐,是当数据不是对齐数的整数倍的时候,进行调整,使得对象的整体大小是对齐数的整数倍方便寻址。典型的以空间换时间的思想。
其中对象标记和类型指针统称为Java对象头。

Class Metadata Adress(类型指针):指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的数据
MarkWord(对象标记):默认存储对象的hashcode,分带年龄,锁状态标志位,线程持有的锁,偏向线程ID等。

MarkWord(对象标记)主要与以下几个概念有关:

重入

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入。自己持有锁时可以再次请求持有锁。

自旋锁
  • 许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得
  • 通过让线程执行忙循环等待锁的释放,不让出CPU
  • 缺点:若锁被其他线程长时间占用,会带来许多性能上的开销
自适应自旋锁
  • 自旋次数不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者状态来决定
锁优化
  • 锁消除
    • JIT编译时,对运行的上下文进行扫描,对于线程的私有变量,不存在并发问题,没有必要加锁,即使加锁编译后,也会去掉。
  • 锁粗化
    • 通过扩大加锁的范围来避免反复加锁减锁,当一个循环中存在加锁操作时,可以将加锁操作提到循环外面执行,一次加锁代替多次加锁,提升性能。
synchronized的四种状态
  • 无锁,偏向锁,轻量级锁,重量级锁

  • 锁膨胀的方向:无锁->偏向锁->轻量级锁->重量级锁

偏向锁:
  • 多数情况下,锁不存在多线程竞争,总是由同一线程多次获得
  • 如果一个线程获得了锁,那么锁就会进入锁偏向模式,此时MarkWord的结构也变为了偏向锁结构,当该线程再次请求锁时,无需在做任何同步操作,即获取锁的过程只需要检查MarkWord的锁标记为偏向锁以及当前线程ID等于MarkWord的ThreadID即可,这样就省去了大量有关申请的操作。

偏向锁的获取:

  1. 首先检测是否为可偏向状态(锁标识是否设置成1,锁标志位是否为01).
  2. 如果处于可偏向状态,测试Mark Word中的线程ID是否指向自己,如果是,不需要再次获取锁,直接执行同步代码。
  3. 如果线程Id,不是自己的线程Id,通过CAS获取锁,获取成功表明当前偏向锁不存在竞争,获取失败,则说明当前偏向锁存在锁竞争,偏向锁膨胀为轻量级锁。

偏向锁的撤销:
偏向锁只有当出现竞争时,才会出现锁撤销。

  1. 等待一个全局安全点,此时所有的线程都是暂停的,检查持有锁的线程状态,如果能找到说明当前线程还存活,说明还在执行同步块中的代码,首相将该线程阻塞,然后进行锁升级,升级到轻量级锁,唤醒该线程继续执行代同步码。
  2. 如果持有偏向锁的线程未存活,将对象头中的线程置null,然后直接锁升级。
轻量级锁:
  • 轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用时,偏向锁就会升级为轻量级锁
  • 适用的场景:线程交替执行同步块,多个线程不会在同一时刻来竞争同一把锁。
  • 若存在同一时间访问同一锁情况,就会导致轻量级锁膨胀为重量级锁

轻量级锁的获取:

  1. 在线程的栈帧中创建用于存储锁记录得空间,
  2. 并将Mark Word复制到锁记录中,(这一步不论是否存在竞争都可以执行)。
  3. 尝试使用CAS将对象头中得Mark word字段变成指向锁记录得指针。
  4. 操作成功,不存在锁竞争,执行同步代码。
  5. 操作失败,锁已经被其它线程抢占了,这时轻量级锁膨胀为重量级锁。

轻量级锁得释放:

  • 反替换,使用CAS将栈帧中得锁录空间替换到对象头,成功没有锁竞争,锁得以释放,失败说明存在竞争,那块指向锁记录得指针有别的线程在用,因此锁膨胀升级为重量级锁。
重量级锁:
  • 重量级锁描述同一时刻有多个线程竞争同一把锁。
  • 当多个线程共同竞争同一把锁时,竞争失败得锁会被阻塞,等到持有锁的线程将锁释放后再次唤醒阻塞的线程,因为线程的唤醒和阻塞是一个很耗费CPU资源的操作,因此此处采取自适应自旋来获取重量级锁。

synchronized底层实现的原理与锁的膨胀_第2张图片

锁的内存语义
  • 当线程释放锁时,java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;
  • 而当线程获取锁时,java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量

文章部分内容转载自:
monitor相关
markword相关

你可能感兴趣的:(Java学习)