第十一章 synchronized 与锁升级
11.1 面试题
-
谈谈你对 synchronized 的理解?
-
synchronized 的锁升级,聊一聊
-
synchronized 实现原理,monitor 对象什么时候生成的?知道 monitor 的 monitorenter 和 monitorexit 这两个是怎么保证同步的嘛?或者说这两个操作计算机底层是如何执行的?
-
偏向锁和轻量锁有什么区别?
11.2 路线总纲
高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁
synchronized 锁优化的背景
-
用锁能够实现数据的安全性,但是会带来性能的下降
-
无锁能够基于线程并行提升程序性能,但是会带来安全性下降
锁升级的过程
无锁 ----> 偏向锁 ----> 轻量级锁 ----> 重量级锁
synchronized 锁
由对象头中的 Mark Word 根据锁标志位的不同而被复用及锁升级策略

11.3 synchronized 的性能变化
Java5 以前,只有 synchronized,这个是操作系统级别的重量级操作
-
重量级锁,假如锁的竞争比较激烈的话,性能下降
-
Java5 之前,用户态和内核态之间的切换

Java 的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与内核态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作
在 Java 早起版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock(系统互斥量)来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。Java 6 之后,为了减少获得锁和释放锁带来的性能消耗,引入了轻量级锁和偏向锁
为什么每一个对象都可以成为一个锁?
C++ 源码:ObjectMonitor.java -> ObjectMonitor.cpp -> ObjectMonitor.hpp

每个对象天生都带着一个对象监视器,每一个被锁住的对象都会和 Monitor 关联起来
Monitor 可以理解为一种同步工具,也可以理解为一种同步机制,常常被描述为一个 Java 对象。Java 对象是天生的 Monitor,每一个 Java 对象都有成为 Monitor 的潜质,因为在 Java 的设计中,每一个 Java 对象自打娘胎里出来就带了一个把看不见的锁,它叫做内部锁或者 Monitor 锁
public class MyObject {
public static void main(String[] args) {
Object objLock = new Object();
new Thread(() -> {
synchronized (objLock) {
//...
}
}, "t1").start();
}
}
Monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高
Monitor(监视器锁)
JVM 中的同步就是基于进入和退出管程(Monitor)对象实现的。每个对象实例都会有一个 Monitor,Monitor 可以和对象一起创建、销毁。
Monitor 是有由 ObjectMonitor 实现,而 ObjectMonitor 是由 C++ 的 ObjectMonitor.hpp 文件实现

Mutex Lock
Monitor 是在 JVM 底层实现的,底层代码是 C++。本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,状态转换需要耗费很多的处理器时间,成本非常高。所以 synchronized 是 Java 语言中的重量级操作
Monitor 与 Java 对象以及线程是如何关联?
-
如果一个 Java 对象被某个线程锁住,则该 Java 对象的 Mark Word 字段中 Lock Word 指向 Monitor 的起始地址
-
Monitor 的 Owner 字段会存放拥有相关联对象锁的线程 id
Mutex Lock 的切换需要从用户态转换到内核态中,因此转换需要耗费很多的处理器时间
结合之前的 synchronized 和对象头说明

Java 6 开始,优化 synchronized
-
Java6 之后,为了减少获得锁和释放锁带来的性能消耗,引入了轻量级锁和偏向锁
-
需要有个逐步升级的过程,别一开始就捅到重量级锁
11.4 synchronized 锁种类及升级步骤
11.4.1 多线程访问情况
- 只有一个线程来访问,有且为 Only One
- 由两个线程(2 个线程交替访问)
- 竞争激烈,更多线程来访问
11.4.2 升级流程
synchronized 用的锁是存在 Java 对象头里的 Mark Word 中,锁升级功能主要是依赖 Mark Word 中锁标志位和释放偏向锁标志位

锁指向,请牢记
-
偏向锁:Mark Word 存储的是偏向的线程 ID
-
轻量锁:Mark Word 存储的是指向线程栈中 Lock Record 的指针
-
重量锁:Mark Word 存储的是指向堆中的 Monitor 对象(系统互斥量指针)
11.4.3 无锁
初始状态,一个对象被实例化后,如果还没有任何线程竞争锁,那么它就是无锁状态(001)

public class SynchronizedUpDemo {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}

31 位 hashCoode(如果有调用)
生成
public class SynchronizedUpDemo {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
o.hashCode();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}

解读
public class SynchronizedUpDemo {
public static void main(String[] args) {
Object o = new Object();
System.out.println("10进制:" + o.hashCode());
System.out.println("16进制:" + Integer.toHexString(o.hashCode()));
System.out.println("2进制:" + Integer.toBinaryString(o.hashCode()));
//31 位 hashCode
//2 进制: 1110101100000101000101000001111
// 01110101100000101000101000001111
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}

11.4.4 偏锁
偏向锁:单线程竞争
当线程 A 第一次竞争到锁时,通过修改 Mark Word 中偏向线程 ID、偏向模式。如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步
-
当一段同步代码一直被同一个线程多次访问,由于只有一个线程,那么该线程在后续访问时便会自动获得锁
-
同一个老客户来访,直接老规则行方便
-
看看多线程卖票,同一个线程获取体会一下
Hotspot 的作者经过研究发现,大多数情况下:
多线程的情况下,锁不仅不存在多线程竞争,还存在锁由同一个线程多次获得的情况
偏向锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步时提高性能
备注:
偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也即偏向锁在资源没有竞争情况下消除了同步语句,懒的连 CAS 操作都不做了,直接提高程序性能

偏向锁的持有
理论落地:
在实际应用运行过程中发现,”锁总是同一个线程持有,很少发生竞争“,也就是说锁总是被第一个占用它的线程拥有,这个线程就是锁的偏向线程
那么只需要在锁第一次被拥有的时候,记录下偏向线程 ID,这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接会去检查锁的 Mark Word 里面是不是放的自己的线程 ID)
如果相等,表示偏向锁时偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程 ID 与当前线程 ID 是否一致,如果一致直接进入同步。无需每次加锁解锁都去 CAS 更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高
如果不等,表示发生了竞争,锁已经不是总是偏向于同一个线程了,这个时候会尝试使用 CAS 来替换 Mark Word 里面的线程 ID 为新线程的 ID
竞争成功,表示之前的线程不存在了,Mark Wold 里面的线程 ID 为新线程的 ID,锁不会升级,仍然为偏向锁
竞争失败,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁
注意,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程时不会主动释放偏向锁的
技术实现:
一个 synchronized 方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的 Mark Word 中将偏向锁修改状态位,同时还会有占用前 54 位来存储线程指针作为标识。若该线程再次访问同一个 synchronized 方法时,该线程只需要去对象头的 Mark Word 中判断一下是否有偏向锁指向本身的 ID,无需再进入 Monitor 去竞争对象了
偏向锁的操作不用直接捅到操作系统,不涉及用户到内核转换,不必要直接升级为最高级,我们以一个 account 对象的“对象头”为例

假如有一个线程执行到 synchronized 代码块的时候,JVM 使用 CAS 操作把线程指针 ID 记录到 Mark Word 当中,并修改偏向标识,表示当前线程就获得该锁。锁对象变成偏向锁(通过 CAS 修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁

这时线程获得了锁,可以执行同步代码块。当该线程第二次到达同步代码块时会判断此时持有锁的线程是否还是自己(持有锁的线程 ID 也在对象头里),JVM 通过 account 对象的 Mark Word 判断:当前线程 ID 还在,说明还持有着这个对象的锁,就可以继续进入临界区工作。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。结论:JVM 不用和操作系统协商设置 Mutex(争取内核),它只需要记录下线程 ID 就表示自己获得了当前锁,不用操作系统介入
上述就是偏向锁,在没有其他线程竞争的时候,一直偏向偏心当前线程,当前线程可以一直执行
偏向锁 JVM 命令
java -XX:+PrintFlagsInitial |grep BiasedLock*

BiasedLockingStartupDelay:偏向锁默认延时 4 秒钟
UseBiasedLocking :偏向锁默认开启
重要参数说明
实际上偏向锁在 JDK1.6 之后是默认开启的,但是启动时间有延时
所以需要添加参数-XX:BiasedLockingStartupDelay=0,让其在程序启动时立刻启动
开启偏向锁:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:关闭之后程序默认会直接进入 ------> 轻量级锁状态
-XX:-UseBiasedLocking
锁升级的过程
无锁 ----> 偏向锁 ----> 轻量级锁 ----> 重量级锁
演示代码
标记位
public class SynchronizedUpDemo {
public static void main(String[] args) {
//biased
Object o = new Object();
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}

由于延时配置,导致未能出现正确的结果
方法一:关闭延时参数
添加虚拟机参数 -XX:BiasedLockingStartupDelay=0

方法二:延迟启动
程序启动后,获得锁之前,进行延迟处理
public class SynchronizedUpDemo {
/**
* 演示偏向锁,请开启 -XX:BiasedLockingStartupDelay=0 即可
*/
public static void main(String[] args) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//biased
Object o = new Object();
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
结果与方法一相同
线程 ID 的值
public class SynchronizedUpDemo {
/**
* 演示偏向锁,请开启 -XX:BiasedLockingStartupDelay=0 即可
*/
public static void main(String[] args) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//biased
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}

可以看到,锁状态为 101 可偏向锁状态,只是由于 o 对象未用 synchronized 加锁,所以线程 id 是空的。其余数据跟上述无锁状态一样
public class SynchronizedUpDemo {
/**
* 演示偏向锁,请开启 -XX:BiasedLockingStartupDelay=0 即可
*/
public static void main(String[] args) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//biased
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
System.out.println("=============");
new Thread(() -> {
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}, "t1").start();
}
}

偏向锁带线程 ID 情况,第一行中后面不再是 0 了,有了线程 ID 的值
开始有第 2 个线程来抢夺了
偏向锁的撤销
当有另外线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级为轻量级锁
竞争线程尝试 CAS 更新对象头失败,会等待到全局安全点(此时不会执行任何代码)撤销偏向锁
偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销
撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行
- 第一个线程正在执行 synchronized 方法(处于同步块),它还没有执行完,其他线程来抢夺,该偏向锁会被取消掉并出现锁升级,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁
- 第一个线程执行完成 synchronized 方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向

总体步骤流程

题外话
Java15 逐步废弃偏向锁
11.4.5 轻锁
轻量级锁:多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻塞
有线程来参与锁的竞争,但是获取锁的冲突时间极端
本质就是自旋锁 CAS

轻量级锁是为了在线程几乎交替执行同步块时提高性能
主要目的:在没有多线程竞争的前提下,通过 CAS 减少重量级锁使用操作系统互斥产生的性能消耗,说白了先自旋,不行才升级阻塞
升级时机:当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁
假如线程 A 已经拿到锁,这时线程 B 又来抢该对象的锁,由于该对象的锁已经被线程 A 拿到,当前该锁已是偏向锁了
而线程 B 在争抢时发现对象头 Mark Word 中的线程 ID 不是线程 B 自己的线程 ID(而是线程 A),那线程 B 就会进行 CAS 操作希望能获得锁
此时线程 B 操作中有两种情况:
如果锁获取成功,直接替换 Mark Word 中的线程 ID 为 B 自己的 ID(A -> B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程“被”释放了锁),该锁会保持偏向锁状态,A 线程 Over,B 线程上位

如果锁获取失败,则偏向锁升级为轻量级锁(设置偏向标识为 0 并设置锁标记为 00),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程 B 会进入自旋等待获得该轻量级锁

轻量级锁的加锁
JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,官方称为 Displaced Mark Word。若一个线程获得锁时发现是轻量级锁,会把锁的 Mark Word 复制到自己的 Displaced Mark Word 里面。然后线程尝试用 CAS 将锁的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示 Mark Word 已经被替换成了其他线程的锁记录,说明在与其他线程竞争锁,当前线程就尝试使用自旋来获取锁
自旋 CAS:不断尝试去获取锁,能不升级往上捅,尽量不要阻塞
轻量级锁的释放
在释放锁的时候,当前线程会使用 CAS 操作将 Displaced Mark Word 的内容复制回锁的 Mark Word 里面,如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么 CAS 操作会失败,此时会释放锁并唤醒被阻塞的线程
演示代码
如果关闭偏向锁,就可以直接进入轻量级锁
-XX:-UseBiasedLocking
public class SynchronizedUpDemo {
/**
* 关闭偏向锁,关闭之后程序默认会进入轻量级锁状态
* -XX:-UseBiasedLocking
*/
public static void main(String[] args) {
Object o = new Object();
new Thread(() -> {
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}, "t1").start();
}
}

总体步骤流程

自旋达到一定次数和程度
Java6 之前
-
默认启用,默认情况下自旋的次数是 10 次
修改方式如下:
-XX:PreBlockSpin=10 -
或者自旋线程数超过 CPU 核数一半
了解即可,别用了
Java6 之后
自适应自旋锁的大致原理
线程如果自旋成功了,那下次自旋的最大次数会增加,因为 JVM 认为既然上次成功了,那么这一次也很大概览会成功
反之,如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免 CPU 空转
自适应意味着自旋的次数不是固定不变的,而是根据
-
同一个锁上一次自旋的时间
-
拥有锁线程的状态来决定
轻量锁与偏向锁的区别和不同
-
争夺轻量级锁失败时,自旋尝试抢占锁
-
轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁
11.4.6 重锁
有大量的线程参与锁的竞争,冲突性很高

Java 中 synchronized 的重量级锁,是基于进入和退出 Monitor 对象实现的。在编译时会将同步块的开始位置插入 monitor enter 指令,在结束位置插入 monitor exit 指令
当线程执行到 monitor enter 指令时,会尝试获取对象所对应的 Monitor 所有权,如果获取到了,即获取到了锁,会在 Monitor 的 owner 中存放当前线程的 ID,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个 Monitor
演示代码
public class SynchronizedUpDemo {
/**
* 重量级锁
*/
public static void main(String[] args) {
Object o = new Object();
new Thread(() -> {
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}, "t1").start();
new Thread(() -> {
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}, "t2").start();
new Thread(() -> {
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}, "t3").start();
}
}

小总结
锁升级发生后,hashCode 值
锁升级为轻量级或重量级锁后,Mark Word 中保存的分别是线程栈帧里的锁记录指针和重量级锁指针,已经没有位置再保存哈希码,GC 年龄了,那么这些信息被移动到哪里去了呢?
用书中的一段话来描述锁和 hash code 之前的关系
在 Java 语言里面一个对象如果计算过哈希值,就应该一直保持该值不变(强烈推荐但不强制,因为用户可以重载hashCode()方法,按自己的意愿返回哈希码),否则很多依赖对象哈希码的 API 都可能存在出错风险。而作为绝大多数对象哈希码来源的Object::hashCode()方法,返回的时对象的一致性哈希码(Identity Hash Code),这个值是能强制保证不变的,它通过在对象头中存储计算结果来保证第一次计算之后,再次调用该方法取到的哈希值永远不会再发生改变。因此,当一个对象已经计算过一致性哈希码后,它就再也无法进入偏向锁状态了;而当一个对象当前正处在偏向锁状态,又收到需要计算其一致性哈希码请求时,它的偏向状态会被立刻撤销,并且锁会膨胀为重量级锁。在重量级锁的实现中,对象头指向了重量级锁的位置。代表重量级锁的 ObjectMonitor 类里有字段可以记录非加锁状态(标志位为“01”)下的 Mark Word,其中自然可以存储原来的哈希码
在无锁状态下,Mark Word中可以存储对象的 identity hash code 值。当对象的hashCode()方法第一次被调用时,JVM 会生成对应的 identity hash code 值并将该值存储到 Mark Word 中
对于偏向锁,在线程获取偏向锁时,会用 Thread ID 和 epoch 值覆盖 identity hash code 所在的位置。如果一个对象的hashCode() 方法已经被调用过一次之后,这个对象不能被设置偏向锁。因为如果可以的话,那 Mark Word 中的 identity hash code 必然会被偏向线程 ID 给覆盖,这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致
升级为轻量级锁时,JVM 会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象的 Mark Word 拷贝,该拷贝中可以包含 identity hash code,所以轻量级锁可以和 identity hash code 共存,哈希码和 GC 年龄自然保存在此,释放锁后会将这些信息写回到对象头
升级为重量级锁后,Mark Word 保存的重量级锁指针,代表重量级锁的 ObjectMonitor 类里有字段记录非加锁状态下的 Mark Word,锁释放后也会将信息写回到对象头
演示代码
当一个对象已经计算过 identity hash code,它就无法进入偏向锁状态,跳过偏向锁,直接进入轻量级锁
public class SynchronizedUpDemo {
/**
* 当一个对象已经计算过 identity hash code,
* 它就无法进入偏向锁状态,跳过偏向锁,直接进入轻量级锁
*/
public static void main(String[] args) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object o = new Object();
System.out.println("本应是偏向锁");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
o.hashCode();
synchronized (o) {
System.out.println("本应是偏向锁,但是由于计算过一致性哈希,会直接升级为轻量级锁");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}

偏向锁过程总遇到一致性哈希计算请求,立马撤销偏向模式,膨胀为重量级锁
public class SynchronizedUpDemo {
/**
* 偏向锁过程总遇到一致性哈希计算请求,立马撤销偏向模式,膨胀为重量级锁
*/
public static void main(String[] args) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object o = new Object();
System.out.println("本应是偏向锁");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
o.hashCode();
System.out.println("偏向锁过程中遇到一致性哈希计算请求,膨胀为重量级锁");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}

各种锁优缺点、synchronized 锁升级和实现原理
锁的优缺点的对比
| 锁 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
| 轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗 CPU | 追求响应时间,同步块执行速度非常快 |
| 重量级锁 | 线程竞争不使用自旋,不会消耗 CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较长 |
synchronized 锁升级过程总结:一句话,就是先自旋,不行再阻塞
实际上是把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级(自旋锁 CAS)的形式
synchronized 在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的 Mark Word 来实现的
JDK1.6 之前 synchronized 使用的是重量级锁,JDK1.6 之后进行优化,拥有了无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁的升级过程,而不是无论什么情况都使用重量级锁
偏向锁:适用于单线程的情况,在不存在锁竞争的时候进入同步方法/代码块,则使用偏向锁
轻量级锁:适用于竞争不激烈的情况(这和乐观锁的使用范围类似),存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用 CPU 资源但是相对比使用重量级锁还是更高效
重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这个时候就需要升级为重量级锁
11.5 JIT 编译器对锁的优化
11.5.1 JIT
Just In Time Compiler 即时编译器
11.5.2 锁消除
public class LockClearUpDemo {
static Object objectLock = new Object();
/**
* 锁消除问题
* 从 JIT 角度看相当于无视它,synchronized(o),不存在
* 这个锁对象并没有被共用扩散到其他线程使用
* 极端的说就是根本没有加这个锁对象的底层机器码,清除了锁的使用
*
* 运行结果:
* hello 1274452530 1875855296
* hello 1756277772 1875855296
* hello 749748823 1875855296
* hello 1119442503 1875855296
* hello 1627956625 1875855296
*/
public static void main(String[] args) {
LockClearUpDemo lockClearUpDemo = new LockClearUpDemo();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
lockClearUpDemo.m1();
}, String.valueOf(i)).start();
}
}
public static void m1() {
/*synchronized (objectLock) {
System.out.println("hello");
}*/
//锁消除问题,JIT 编译器会无视它,synchronized(o),每次 new 出来,不存在了,非正常的
Object o = new Object();
synchronized (o) {
System.out.println("hello\t" + o.hashCode() + "\t" + objectLock.hashCode());
}
}
}
11.5.3 锁粗化
public class LockBigDemo {
static Object objectLock = new Object();
/**
* 锁粗化
* 假如方法中首尾相接,前后相邻的都是同一个锁对象,那 JIT 编译器就会把
* 这几个 synchronized 块合并成一个大块,加粗加大范围,一次申请锁使用即可,
* 避免次次的申请和释放锁,提升了性能
*
* 运行结果:
* 111111
* 222222
* 333333
* 444444
*/
public static void main(String[] args) {
new Thread(() -> {
/*synchronized (objectLock) {
System.out.println("111111");
}
synchronized (objectLock) {
System.out.println("222222");
}
synchronized (objectLock) {
System.out.println("333333");
}
synchronized (objectLock) {
System.out.println("444444");
}*/
synchronized (objectLock) {
System.out.println("111111");
System.out.println("222222");
System.out.println("333333");
System.out.println("444444");
}
}, "t1").start();
}
}
11.6 小总结
没有锁:自由自在
偏向锁:唯我独尊
轻量锁:楚汉争霸
重量锁:群雄逐鹿

浙公网安备 33010602011771号