关闭偏向锁延迟
-XX:BiasedLockingStartupDelay=0
关闭偏向锁 程序默认进入轻量级锁状态
-XX:-UseBiasedLocking=false
2.1.2 轻量级锁
2.1.2.1 轻量级锁加锁
线程在执行同步代码块之前,JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程便尝试自旋获得锁。
《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》
【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享
轻量级锁解锁时,使用的是CAS操作将Displaced Mark Word替换回对象头,如果成功则表示没有竞争;如果失败,表示当前锁存在竞争,锁就会膨胀为重量级锁。 注意:由于自旋过程消耗CPU,为了避免无用的自旋,一当升级为重量级锁,那么就不会再恢复到轻量级锁状态。当前锁出于重量级锁状态时,其他线程尝试获取锁,都会被阻塞,当持有锁的线程释放锁后会唤醒这些线程,重新进行锁的争夺。
2.1.3 锁的优缺点对比
锁的优缺点对比
| 锁 | 优点 | 缺点 | 适用场景 |
| — | — | — | — |
| 偏向锁 | 加锁和解锁过程不需要额外的消耗,和执行非同步方法相比仅存在纳秒级别的差异 | 如果线程存在锁竞争,会带来额外的锁撤销开销 | 适用于只有一个线程访问同步代码块 |
| 轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间,同步代码块执行速度非常快 |
| 重量级锁 | 线程竞争不适用自旋,不会消耗CPU | 线程阻塞,响应时间慢 | 追求吞吐量,同步块执行速度较长 |
在Java中可以通过锁和循环CAS的方式实现原子操作
3.1 使用循环CAS实现原子操作
JVM中的CAS操作利用了处理器提供的CMPXCHG指令实现。自旋CAS实现的基本思路就是循环进行CAS操作知道成功为止,示例代码实现一个安全的计数器和非安全的计数器。
package com.liziba;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
-
@auther LiZiBa
-
@date 2021/2/28 17:39
-
@description: 计数器实现
**/
public class Counter {
// 安全计数器统计数
private AtomicInteger atomicInteger = new AtomicInteger(0);
// 非安全计数器统计数
private int i = 0;
public static void main(String[] args) {
final Counter cas = new Counter();
List threads = new ArrayList<>(600);
long start = System.currentTimeMillis();
for (int j = 0; j < 100; j++) {
Thread t = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
// 非安全计数器
cas.count();
// 安全计数器
cas.safeCount();
}
});
threads.add(t);
}
// 启动线程
threads.forEach(t -> t.start());
// join等待所有线程执行完毕
for (Thread t : threads) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 输出不安全计数器结果、安全计数器结果、程序执行时间
System.out.println(cas.i);
System.out.println(cas.atomicInteger.get());
System.out.println(System.currentTimeMillis() - start);
}
/**
- 安全计数器
*/
private void safeCount() {
i++;
}
/**
- 非安全计数器
*/
private void count() {
for (;😉 {
int i = atomicInteger.get();
// CAS 增加
// 注意使用 ++i
boolean set = atomicInteger.compareAndSet(i, ++i);
// 设置成功退出死循环
if (set) {
break;
}
}
}
}
运行结果
711319
1000000
265
Process finished with exit code 0
JDK 1.5开始,JDK并发包提供了一些类支持原子操作,如AtomicBoolean,AtomicInteger,AtomicLong,对应不同类型的原子操作,这些类提供了非常有用的工具方法,比如原子自增和自减等等。
3.1.1 CAS 实现原子操作的三大问题
-
ABA问题:因为CAS需要在操作值的时候,检查值是否发生了变化,如果没有变化则更新,但是如果一个值从A修改为B又修改为A,那么使用CAS就无法发现值发生了变化,但实际上发生了变化。解决方案如下
-
使用版本号解决,将原本的A–>B–>A问题变成1A–>2B–>3A则可以解决
-
使用JDK Atomic包里提供的AtomicStampedReference来解决ABA问题,这个类compareAndSet方法会首先比较当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等则以原子方式替换,源码如下。
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
-
循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的开销。解决这个问题需要JVM能支持处理器提供的pause指令,效率会有一定的提升。pause指令的两个作用如下
-
延迟流水线执行指令(de-pipeline),使CPU不会消耗过多执行资源
-
避免在退出循环时内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空
-
只能保证一个共享变量的原子操作:对多个共享变量进行操作的时候CAS无法保证原子性,解决方案如下
-
使用锁
-
变量合并
-
使用JDK提供的AtomicReference类来保证引用对象之间的原子性,将多个变量放置于对象中
3.2 使用锁机制实现原子操作
锁机制保证了只有获得了锁的线程才能操作锁定的内存区域。JVM内部实现了很多锁,偏向锁、轻量级锁、重量级锁。但是除了偏向锁,JVM实现锁的方式都使用了循环CAS机制,即一个线程想进入同步代码块的时候,使用循环CAS的方式获取锁,当它退出同步代码块的时候使用循环CAS来释放锁
参考资料
- 《Java并发编程的艺术》-- 方腾飞 魏鹏 程晓明 著
- 百度百科
- CSDN部分博客