面试必备java synchronized锁的升级

首先祭出下图,上图是线程获取锁和锁升级的基本流程(来自 这里
面试必备java synchronized锁的升级_第1张图片

1 了解 synchronized

synchronized 是 Java 中的关键字,是利用锁的机制来实现同步的。是Java内置的机制,是JVM层面的。
jdk 1.6以前synchronized 关键字只表示重量级锁。
在jdk1.6开始 ,对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
锁主要存在四中状态,依次是:无锁状态、偏向锁状、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。
其中锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
Hotspot 在 1.8 开始有了锁降级

了解Java对象头”、“Monitor”。

我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。

Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

我们可以抽象的理解为每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
面试必备java synchronized锁的升级_第2张图片

  • CAS无锁

    • 无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

      无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

  • 偏向锁

    • 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

      在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。

      当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。

      偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

      偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

  • 轻量级锁

    • JVM 会给线程的栈帧中创建一个叫锁记录 Lock Record 的空间,把对象头 Mark Word 复制到该空间里(Displaced Mark Word),并通过 CAS 尝试把原对象头 Mark Word 中锁记录指针指向该锁记录。如果成功,表示线程拿到了锁。如果失败,则进行自旋(自旋锁),自旋超过一定次数时升级为重量级锁,这时该线程会被内核挂起。
  • 重量级锁

    • 升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。重量级锁就是通过内核来操作线程。因为频繁出现内核态与用户态的切换,会严重影响性能。

2 synchronized 锁升级过程

面试必备java synchronized锁的升级_第3张图片
1 假设a,b两个线程,a 获得了资源偏向锁
2 b访问资源 检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
3 如果线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);
4 通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,
5.判断当前对象是否处于无锁状态,若是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方叫做Displaced Mark Word),否认执行步骤3
6.JVM利用CAS操作尝试将对象的Mark Word更新为指向 Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作,如果失败则执行步骤7
7.判断当前对象的MarkWord是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,发生自旋,自旋到一定次数,失败后锁膨胀为重量级锁。这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态

轻量级解锁时,通过 CAS 操作用线程中复制的 Displaced Mark Word 中的数据替换对象当前的 Mark Word
如果替换成功,整个同步过程就完成了
如果替换失败,说明有其他线程尝试过获取该锁,那就在释放锁的同时,唤醒被挂起的线程

当一个线程已经持有偏向锁,而另外一个线程尝试竞争偏向锁时,CAS 替换 ThreadID 操作失败,则开始撤销偏向锁。偏向锁的撤销,需要等待原持有偏向锁的线程到达全局安全点(在这个时间点上没有字节码正在执行),暂停该线程,并检查其状态
如果原持有偏向锁的线程不处于活动状态或已退出同步代码块,则该线程释放锁。将对象头设置为无锁状态(锁标志位为’01’,是否偏向标志位为’0’)
如果原持有偏向锁的线程未退出同步代码块,则升级为轻量级锁(锁标志位为’00’)

优点 缺点 使用场景
偏向锁 加锁和解锁不需要额外的消耗 如果线程出现竞争,会带来额外的锁撤销的消耗 适用于当前只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高响应速度 如果始终得不到锁竞争的线程,使用自旋消耗CPU 追求响应时间,同步块执行速度快
重量级锁 线程竞争不适用自旋,不会消耗CPU 阻塞线程,响应时间缓慢 追求吞吐量

结语

关于synchronized原理方面的知识,网上的博客大多都是参考《深入理解Java虚拟机》以及《Java并发编程艺术》这两本书上的内容,本文也不例外。至于为啥还要写?重在总结过程中对知识的思考与考究。

参考:
不可不说的Java“锁”事
浅析 Synchronized的底层实现及锁升级
java并发笔记四之synchronized 锁的膨胀过程(锁的升级过程)深入剖析
synchronized的锁升级/锁膨胀

你可能感兴趣的:(基础,study,多线程,java,面试,jvm,后端)