苏三的免费八股文网站:
在刚开始学习 Java 并发编程的过程中,一遇到多线程,我们就会使用 synchronized 关键字。
在 JDK1.5 之前,Synchronized 是一个重量级锁,效率不尽如人意。
JDK1.6 对 Synchronized 锁进行了升级优化,引入了偏向锁和轻量级锁,提高了获取锁和释放锁的效率。下面我们来看一看 Synchronized 的底层实现原理吧。
Synchronized 的底层实现原理
同步原理
我们先来反编译下面的 method1 方法:
public
void
method1
()
{
synchronized
(
this
) {
System.out.println(
"This is the synchronized"
);
}
}
在下面,我们可以看到反编译后的 method1 代码:
从上面代码执行过程中,我们看到代码块同步是使用 monitorenter 和 monitorexit 指令实现的。
monitorenter 指令是在编译后插入到同步代码块的开始位置,monitorexit 是插入到方法结束的位置或者异常处。
JVM 要保证每个 monitorenter 必须有对应的 monitorexit 与之配对。任何对象都有一个 monitor 对象与之关联,当 monitor 被对象持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。
我们再看一下 Synchronized 方法同步的步骤:
public synchronized void method2() {
System.out.println("This is the synchronized method");
}
在上面编译过的 method2() 方法的执行过程中,Synchronized 方法的同步是使用另外一种方式实现的,我们可以看到它被翻译成普通的方法调用和返回 invokevirtual、return 指令。
一个被 Synchronized 修饰的方法在 JVM 底层并没有与之对应的字节码指令。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了执行线程将先获取 monitor,获取成功之后,才能执行方法,方法执行完成再释放 monitor。
Java 对象头的概念
接下来,我们再来了解一下 Java 对象头的概念。Synchronized 用的锁就是存放在 Java 对象头里。如果对象是数组类型,则虚拟机用 3 个字宽(Word)存储对象头,如果对象是非数组类型,则用 2 字宽存储对象头。在 32 位虚拟机中,1 字宽等于 4 字节,即 32bit。
我们看下面的表格,Java 对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。Mark Word 用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。Class Pointer 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
Java 对象头里的 Mark Word 里默认存储对象的 HashCode、分代年龄和锁标记位。我们来看一下 32 位 JVM 的 Mark Word 的默认存储结构:
在运行期间,Mark Word 里存储的数据会随着锁标志位的变化而变化。Mark Word 可能变化为存储以下 4 种数据,我们来看一下存储结构:
在 64 位虚拟机下,Mark Word 是 64bit 大小的,我们看一下它的存储结构:
我们从上面的表格中看到了引入的偏向锁和轻量级锁。锁级别从低到高依次为:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这 4 种状态是会随着竞争情况而逐渐升级的。锁可以升级但不能降级,目的是提高获得锁和释放锁的效率。
下面我们来了解一下锁升级的流程。
锁升级
Synchronized 内部有一个隐藏的锁升级流程,正是因为这个流程的存在,使得 Synchronized 得以发挥它的高性能特性。锁升级中最重要的 2 个升级就是偏向锁和轻量级锁,下面我们分别展开讨论:
偏向锁
通常情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程更容易获得锁而引入了偏向锁。
所谓偏向锁,就是当一个线程访问同步代码块并获取锁时,会在对象头存储锁偏向的线程 ID。这样,以后该线程在进入和退出同步块时,就不需要进行 CAS 操作来加锁和解锁,只需要简单地测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。
如果测试成功,则表示线程已经获得了锁;如果测试失败,则需要再测试一下 Mark Word 中偏向锁的标识是否设置成 1(表示当前是偏向锁)。如果没有设置,则使用 CAS 竞争锁;如果设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程。
偏向锁在 Java6 和 Java7 是默认启用的,但它在应用程序启动几秒钟之后才会被激活,我们可以配置 JVM 参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果确定应用程序里所有的锁通常情况下处于竞争状态,我们可以配置如下的 JVM 参数关闭偏向锁,之后程序默认会进入轻量级锁状态:
-XX:-UseBiasedLocking=
false
轻量级锁
轻量级锁不是用来替代传统的重量级锁的,而是在没有多线程竞争的情况下,使用轻量级锁能够减少性能消耗,但是当多个线程同时竞争锁时,轻量级锁会膨胀为重量级锁。
我们先来看一下轻量级锁的加锁过程。在线程在执行同步块之前,JVM 会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的 Mark Word 复制到锁记录中,官方称为 Displaced Mark Word。然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获
得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
轻量级解锁时,会使用原子的 CAS 操作将 Displaced Mark Word 替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
因为自旋会消耗 CPU,为了避免无用的自旋,一旦锁升级到重量级锁,就不会再恢复到轻量级锁状态。
当锁处于这个状态下,其他线程尝试获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的竞争。
下图是两个线程同时争夺锁,导致轻量级锁膨胀的流程。
锁的优缺点对比
对于偏向锁、轻量级锁和重量级锁这三者的优缺点,以及适用场景,我们可以通过下面的表格得到一个直观的了解:
总结
最后,我们来总结一下所讲的主要内容。
首先,我们一起学习了 Synchronized 的底层实现,Synchronized 作为一个关键字以它极简的语法也带来了易读性;之后,我带你了解了偏向锁的初始化、撤销、关闭操作和轻量级锁的加锁、解锁过程;最后,我带你分析了不同锁的优缺点及适用场景,这些对你理解为什么 Synchronized 具备高性能是非常关键的。
此外,不同锁的,如偏向锁、轻量级锁对使用者来说是透明的,这也体现了 Synchronized 的简单性。
随着 JDK 的不断发展,Synchronized 已经做了足够多的性能优化。
Synchronized 从一个开销很大的重量级锁被优化成一个可自动适配场景的“智能”锁,它可以根据场景转换成偏向锁、轻量级锁,万不得已的情况下才会转换成重量级锁。
它的应用场景也随着这些特性逐渐丰富起来,在很多高并发场景甚至替代了 ReentrantLock。
最后欢迎
,你将获得:AI开发项目课程、苏三AI项目、
商城微服务实战、秒杀系统实战
、
商城系统实战、秒杀系统实战、代码生成工具、系统设计、性能优化、技术选型、底层原理、Spring源码解读、工作经验分享、痛点问题
、面试八股文
等多个优质专栏。
还有1V1答疑、修改简历、职业规划、送书活动、技术交流。
扫描下方二维码,即可加入星球:
目前星球已经更新了 5200+ 篇优质内容,还在持续爆肝中.....
星球已经被 官方推荐了3次 ,收到了小伙伴们的一致好评。戳我加入学习,已有 1600+ 小伙伴加入学习。
苏三的免费八股文网站: