JavaEE多线程进阶(上)
1. 常见锁策略
用锁前需了解锁
1.1 乐观锁vs悲观锁
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别⼈会修改,所以每次在拿数据的时候都会上锁, 这样别⼈想拿这个数据就会阻塞直到它拿到锁。其他线程想要访问,就阻塞等待,从根源上防止冲突。
如:synchronized、ReentrantLock等。
乐观锁:假设数据⼀般情况下不会产⽣并发冲突,所以在数据进⾏提交更新的时候,才会正式对数据是否产⽣ 并发冲突进⾏检测,如果发现并发冲突了,则让返回⽤⼾错误的信息,让用户决定如何去做。
如:CAS、数据库版本号机制。
1.2 重量级锁vs轻量级锁
重量级锁:依赖操作系统互斥锁(mutex),竞争失败线程直接阻塞。轻量级锁下线程自旋多次抢不到锁就会升级为重量级锁。
轻量级锁:加锁机制尽可能不使⽤mutex,而是尽量在用户态代码完成。实在搞不定了,再使⽤mutex。偏向锁发生竞争,锁还没有释放 → 撤销偏向锁,升级为轻量级锁。
| 轻量级锁 | 重量级锁 | |
|---|---|---|
| 底层机制 | CAS 自旋,用户态 | 操作系统内核互斥锁 |
| 竞争失败行为 | 线程自旋空转 | 线程阻塞挂起 |
| CPU 开销 | 自旋消耗 CPU | 几乎不耗 CPU |
| 开销来源 | CPU 空循环 | 线程上下文切换 |
| 适用场景 | 竞争少,锁持有时间短 | 竞争激烈,锁持有时间长 |
通俗比喻:
- 轻量级锁:原地转圈等。别人拿着锁,你不睡觉,不停尝试抢;如果对方很快用完,你直接拿到;如果对方迟迟不放,你一直转圈很累(耗 CPU)。
- 重量级锁:坐下睡觉等。抢不到就直接睡觉,放弃 CPU;别人用完锁再叫醒你;睡觉不费力气,但是叫醒、睡觉这个动作本身代价很高(上下文切换)。
背诵短句
- 轻量级锁:CAS 自旋,不阻塞,消耗 CPU,适合竞争少、锁时间短;
- 重量级锁:操作系统阻塞线程,有上下文切换开销,适合竞争激烈、锁时间长。
1.3 自旋锁
自旋锁:线程获取锁失败时,不把自己阻塞挂起,循环不停重试抢锁,直到拿到锁。线程不会放弃 CPU,一直在 CPU 上跑循环。
特点:
- 抢不到锁不阻塞,原地循环 CAS。
- 没有线程阻塞、没有操作系统上下文切换。
- 会占用 CPU,一直空转消耗 CPU 资源。
伪代码:
while (抢锁(lock) == 失败) {}
优点:线程不会放弃 CPU,不发生线程阻塞、操作系统调度切换。锁一旦释放,线程可以立刻获取锁,响应快。
缺点:若锁被占用时间较长,线程持续空转,会大量消耗 CPU,而阻塞挂起的线程几乎不消耗 CPU。
1.4 公平锁vs非公平锁
公平锁:线程按照申请锁的先后顺序排队,先到先得,不允许插队。
非公平锁:允许新来的线程直接尝试抢锁,可以插队,不一定按排队顺序获取。
注意:由于操作系统内部的线程调度是随机的,正常情况可以视为非公平锁。
1.5 可重入锁vs不可重入锁
可重入锁:允许一个线程多次获取同一把锁,不会阻塞自己,也叫递归锁,如reentranlock和synchronized。
不可重入锁:简单来说就是把自己锁死,如果一个线程没有释放锁,反而再次申请锁就会锁死自己,操作系统的mutex就是不可重入锁。
1.6 读写锁
(1)读与读之间不会有线程安全问题,直接并发读取。
(2)读与写、写与写之间存在线程安全问题,需要进行互斥。
Java标准库提供ReentrantReadWriteLock类来使用读写锁,ReentrantReadWriteLock.ReadLock 类表⽰⼀个读锁,ReentrantReadWriteLock.WriteLock 类表示⼀个写锁,这两个对象都提供了lock和unlock进行加锁解锁。
代码:
package practice.thread;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class RW{
// 共享数据
public static int data = 0;
// 读写锁对象
private static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读锁
private static ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
// 写锁
private static ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
public RW(){
}
//读操作
public void read(){
readLock.lock();
try {
// 读数据
System.out.println("当前线程"+Thread.currentThread().getName()+"执行读操作,data为:" + data);
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 需要手动解锁(注意)
readLock.unlock();
}
}
//写操作
public void write(int val){
writeLock.lock();
try {
data = val;
System.out.println("当前线程"+Thread.currentThread().getName()+"已执行写操作,data为:"+data);
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}finally{
writeLock.unlock();
}
}
}
public class Demo25 {
public static void main(String[] args) throws Exception{
RW rw=new RW();
new Thread(()->{
rw.read();
},"读1").start();
new Thread(() -> {
rw.read();
}, "读2").start();
new Thread(()->{
rw.write(10);
},"写1").start();
new Thread(() -> {
rw.write(20);
}, "写2").start();
}
}
结果:

2. CAS
2.1 CAS是什么
CAS 全称 Compare And Swap,比较并交换,是硬件 CPU 提供的原子指令,是乐观锁的底层实现基础。
CAS 包含 3 个操作数:
- V:内存中变量实际值
- A:预期旧值
- B:准备写入的新值
执行逻辑:
- 比较内存真实值V与预期值A是否相等
- 如果相等,就把V更新为新值B,返回成功
- 如果不相等,说明已经被其他线程修改,直接返回失败,不做修改
伪代码:
boolean CAS(address, expectValue, swapValue){
if(getMemory(address) == expectValue){
setMemory(address,swapValue);
return true;
}
return false;
}
注意:比较 + 交换这整套动作,是一条 CPU 原子指令,不会被线程打断。Java 通过Unsafe类封装 CAS 操作。
2.2 CAS 的应用
1. 实现原子类
java.util.concurrent.atomic包下面的原子类(AtomicInteger)底层基于 CAS 实现,不需要加锁保证原子性。
getAndIncrement()自增伪代码:
public int getAndIncrement(){
int oldValue = value;
while(!CAS(value,oldValue,oldValue+1)){
oldValue = value;
}
return oldValue;
}
循环执行 CAS,直到修改成功。
2. 实现自旋锁
循环 CAS 尝试抢占锁,抢不到就不断重试。
2.3 CAS 存在的问题 — ABA 问题
1. 什么是 ABA
线程 1 读取变量值为 A,准备执行 CAS 修改;在线程 1 执行 CAS 之前,线程 2 把变量 A 修改为 B,又改回 A。此时内存值依旧是 A,CAS 校验会判定没有发生修改,执行更新;但数据中间已经被篡改过,产生逻辑错误。
举例:账户余额 100,线程 A 要扣款 50;中途线程 B 把余额改成 50,又转账改回 100;此时 A 的 CAS 依旧执行扣款,业务出错。
2. ABA 解决方案
引入版本号(时间戳),每次修改版本号自增;CAS 同时比较值 + 版本号。
Java 提供AtomicStampedReference,内部维护数据 + 版本戳,专门解决 ABA 问题。
2.4 CAS 其他缺点
- 竞争激烈时,循环自旋大量消耗 CPU。
- 只能保证单个变量的原子操作,不能同时操作多个共享变量。
3. synchronized原理
synchronized 是 JVM 实现的锁,锁只能升级,不能降级。
锁状态流转:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
3.1 synchronized 整体特性
- 锁竞争少使用乐观策略,冲突频繁切换悲观锁;
- 锁状态逐级升级;轻量级锁使用自适应自旋;
- 非公平锁、可重入锁、不是读写锁。
3.2 偏向锁
偏向锁的目的:单线程无竞争场景,尽可能消除 CAS 加锁开销。
- 第一个线程获取锁,对象头 Mark Word 记录该线程 ID,标记偏向锁。
- 同一个线程再次进入同步块,直接判断线程 ID,不需要 CAS 操作。
- 释放锁的时候,不会清除偏向标记。标记存在不等于锁被占用。
- 当出现其他线程来竞争锁,立刻撤销偏向锁,升级为轻量级锁。
注意:偏向锁只是对象头的标记,不是线程状态。同一个对象,偏向锁只能记录一个线程 ID。
3.3 轻量级锁
触发时机:偏向锁发生竞争,锁还没有被释放,撤销偏向锁升级轻量级锁。
- 底层基于 CAS + 自适应自旋锁,工作在用户态,尽量不调用操作系统互斥锁 mutex。
- 竞争锁的线程不阻塞,循环 CAS 自旋尝试抢锁。
- JVM 自适应调整自旋次数;如果自旋多次依旧获取不到锁,升级重量级锁。
适用场景:竞争不激烈,锁持有时间很短。优点没有内核态切换;缺点自旋消耗 CPU。
3.4 重量级锁
触发时机:轻量级锁自旋多次抢锁失败,膨胀为重量级锁。
- 依赖操作系统内核提供的mutex互斥锁;
- 获取锁失败的线程停止自旋,被操作系统挂起,进入阻塞队列;
- 锁释放后操作系统唤醒等待的线程,重新竞争锁。
存在用户态↔内核态切换,线程上下文切换开销大;适合锁持有时间长、竞争激烈。
3.5 JVM 锁优化(synchronized 额外优化)
锁消除
JVM 编译器检测到锁对象不会发生多线程竞争,直接把加锁解锁代码消除。
例如单线程环境下StringBuffer的synchronized方法,锁直接消除,减少开销。
锁粗化
如果一段代码频繁多次加锁、释放锁;JVM 会把多次加解锁合并成一次大粒度锁,减少频繁锁操作开销。
锁的粒度
加锁和解锁范围代码越多粒度越粗,返回越细
// t1:粗粒度锁:把整个for循环全部包进synchronized
Thread t1 = new Thread(() -> {
synchronized (locker) {
for (int i = 0; i < 50000; i++) {
count++;
}
}
});
// t2:细粒度锁:只把count++这一行包进synchronized,循环在锁外面
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
synchronized (locker) {
count++;
}
}
});
4. 面试背诵小结
- CAS:CPU 硬件原子指令,比较并交换;乐观锁实现;存在 ABA 问题,使用版本号AtomicStampedReference解决。
- synchronized 锁升级:无锁→偏向锁(单线程无竞争)→轻量级锁(CAS 自旋)→重量级锁(操作系统阻塞),锁只能升级不能降级。
- 自旋锁:抢锁失败循环重试;耗 CPU,无上下文切换;适合锁持有时间短。
- 悲观锁上来就上锁;乐观锁不加锁,更新校验冲突。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)