深入理解Java并发编程:轻量级锁与锁膨胀机制详解
一、轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以 使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是
示例
-
- static final Object obj = new Object();
- public static void method1() {
- synchronized (obj) {
- // 同步块 A
- method2();
- }
- }
- public static void method2() {
- synchronized (obj) {
- // 同步块 B
- }
- }
复制代码
图解加锁过程
二、锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行 ,将轻量级锁变为重量级锁。
示例
-
- static Object obj = new Object();
- public static void method1() {
- synchronized (obj) {
- // 同步块
- }
- }
复制代码
- 1、当Thread-1进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
- 2、这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
- 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址;
- 然后自己进入 Monitor 的。
- 3、当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入
- 即按照 Monitor 地址找到 Monitor 对象
- 设置 Owner 为 null
- 唤醒中 BLOCKED 线程
三、自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋定义
是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
自旋重试成功的情况
线程1 (cpu1上) |
对象Mark |
线程2 (cpu2上) |
- |
10 (重量锁) |
- |
访问同步块,获取monitor |
10 (重量锁) 重量锁指针 |
- |
成功(加锁) |
10 (重量锁) 重量锁指针 |
- |
执行同步块 |
10 (重量锁) 重量锁指针 |
- |
执行同步块 |
10 (重量锁) 重量锁指针 |
访问同步块,获取monitor |
执行同步块 |
10 (重量锁) 重量锁指针 |
自旋重试 |
执行完毕 |
10 (重量锁) 重量锁指针 |
自旋重试 |
成功(解锁) |
无锁 |
自旋重试 |
- |
10 (重量锁) 重量锁指针 |
成功(加锁) |
- |
10 (重量锁) 重量锁指针 |
执行同步块 |
- |
... |
... |
*注意:自旋需要cpu资源,所以适合多核cpu*
自旋重试失败的情况
线程1 (cpu1上) |
对象Mark |
线程2 (cpu2上) |
- |
10 (重量锁) |
- |
访问同步块,获取monitor |
10 (重量锁) 重量锁指针 |
- |
成功(加锁) |
10 (重量锁) 重量锁指针 |
- |
执行同步块 |
10 (重量锁) 重量锁指针 |
- |
执行同步块 |
10 (重量锁) 重量锁指针 |
访问同步块,获取monitor |
执行同步块 |
10 (重量锁) 重量锁指针 |
自旋重试 |
执行同步块 |
10 (重量锁) 重量锁指针 |
自旋重试 |
执行同步块 |
10 (重量锁) 重量锁指针 |
自旋重试 |
执行同步块 |
10 (重量锁) 重量锁指针 |
阻塞 |
- |
... |
... |
+ 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
+ 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
+ Java 7 之后不能控制是否开启自旋功能
|