synchronized 在 JDK1.5时性能是比较低的,然而在后续版本中经过各种优化迭代,它的性能得到了提升。
synchronized 核心优化方案主要包含以下4个:
锁膨胀其实指的就是锁升级。synchronized 从 无锁 升级到 重量级锁。这个过程是不可逆的,也就是只能升级,不能降级。
JDK1.6之前,synchronized 是重量级锁,也就是说 synchronized 在释放和获取锁的时候 会从用户态转换成 内核态。
1.5时,synchronized 通过监视器锁 monitor 来实现,而monitor又是依赖于 底层操作系统的 mutex lock(互斥锁)实现的。
因此就会造成很高的成本。
用户态: 用户在执行用户自己的代码时,则称其处于用户运行态。
内核态: 当一个任务进程执行系统调用而陷入内核代码中执行时,就称进程处于内核运行态。此时处理器处于特权级最高的内核代码中执行。
为什么要区分用户态 和 内核态?
假设没有用户态和内核态之分,程序就可以随意读写硬件资源,如果程序一不小心写到了不该写的地方,可能导致系统奔溃。
而有了用户态和内核态后,程序执行时会进行一系列的验证和校验,确认没问题才会正常操作资源。
MarkWord中 四种锁的状态
无锁:锁标志位 01 是否偏向锁 0
偏向锁:锁标志位 01 是否偏向锁 1
轻量级锁: 锁标志位 00
重量级锁: 锁标志位 10
偏向锁指的是,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程不需要触发同步的,这种情况会给线程加一个偏向锁。
偏向锁的执行流程
一个线程访问同步代码块,并获取锁时,会在对象头的markword里存储 锁偏向的线程ID,在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测 Mark Word里是否存储着 当前线程的偏向锁,如果Mark Word的线程ID和访问的线程ID一致,则可以直接进入同步代码块执行。如果线程ID不同,则通过CAS尝试获取锁,如果获取成功,进入同步块执行代码,否则,将偏向锁升级为 轻量级锁。
偏向锁的撤销
需要等待全局安全点(这个时间点上没有字节码正在执行),会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后,恢复到未锁定(标志位 01) 或轻量级锁(标志位 00)的状态。
偏向锁的优势 : 偏向锁只在 置换线程ID的时候执行一次CAS原子指令即可。
轻量级锁目的是 在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统(Mutext Lock)产生的性能消耗。
关闭偏向锁或者多个线程竞争偏向锁时,偏向锁会升级为 轻量级锁。
轻量级锁的执行流程
(1)在线程进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为 01),虚拟机首先将在当前线程的栈帧中建立一个名为
锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displace Mark Word。如图2.1
(2)拷贝对象头的MarkWord 复制到锁记录中。
(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的MarkWord 更新为指向Lock Record的指针,并将Lock Record里的owner指针指向Object mark word。如果更新成功,执行步骤(4)
(4)线程持有该对象的锁,并且对象 mark word的锁标志位 设置为 00。表示处于轻量级锁定状态。(如图2.2)
(5)如果更新操作失败了,虚拟机首先会检查对象的MarkWord是否指向当前线程的栈帧,如果是说明,线程已经拥有这个对象的锁了,可以直接进入同步块执行。否则多个线程竞争锁,轻量级锁就会变为重量级锁,锁状态变为 10 。markword存储就是指向重量级锁
(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
图 2.1 轻量级锁CAS操作之前堆栈与对象的状态 图2.2 轻量级锁CAS操作之后堆栈与对象的状态synchronized 依赖监视器 Monitor实现方法同步或者代码块同步的,代码块同步使用的是 monitorenter 和 monitorexit 指令来实现的。
monitorenter 指令是在编译后 插入到同步代码块的开始位置
monitorexit 指令是插入到 方法结束处和异常处的
每个对象都有一个 Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。
public class SynchronizedToMonitorExample {
public static void main(String[] args) {
int count = 0;
synchronized (SynchronizedToMonitorExample.class) {
for (int i = 0; i < 10; i++) {
count++;
}
}
System.out.println(count);
}
}
锁消除指的是某些情况下,JVM虚拟机如果检测到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而提高程序性能的目的。
锁消除的依据是逃逸分析的数据支持,如StringBuffer的append() 方法。或者 Vector的 add()方法。
**虽然我们代码使用的是 线程安全的对象,但是JVM生成字节码判断不会发生逃逸,就会自动替换成不安全的对象 **
public String method() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 10; i++) {
sb.append("i:" + i);
}
return sb.toString();
}
锁粗化: 是指将多个连续的加锁、解锁操作合在一起,扩展成一个范围更大的锁。
public String method() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
// 伪代码:加锁操作
sb.append("i:" + i);
// 伪代码:解锁操作
}
return sb.toString();
}
如果在 for 循环中定义锁,那么锁的范围很小,但每次 for 循环都需要进行加锁和释放锁的操作,性能是很低的;但如果我们直接在 for 循环的外层加一把锁,那么对于同一个对象操作这段代码的性能就会提高很多,
public String method() {
StringBuilder sb = new StringBuilder();
// 伪代码:加锁操作
for (int i = 0; i < 10; i++) {
sb.append("i:" + i);
}
// 伪代码:解锁操作
return sb.toString();
}
锁粗化的作用:如果检测到同一个对象执行了连续的加锁和解锁的操作,则会将这一系列操作合并成一个更大的锁,从而提升程序的执行效率。
自旋锁 : 通过自身循环,尝试获取锁的一种方式。
while (!isLock) {
}
自旋锁优点在于它避免一些线程的挂起和恢复操作,因为挂起线程和恢复线程都需要从用户态转入内核态,这个过程是比较慢的,所以通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。
长时间自旋会造成资源浪费,通常我们会给自旋设置一个固定的值来避免一直自旋的性能开销
自适应自旋锁: 线程自旋的次数不再是一个固定值,而是一个动态改变的值,这个值会根据前一次自旋获取锁的状态来决定此次自旋的次数。
如果线程自旋成功了,则下次自旋的次数会增多,如果失败,下次自旋的次数会减少。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。