名词解释:
指令重排是计算机为了优化执行效率,在不改变单线程程序结果的前提下,对代码的执行顺序进行重新排列的操作。它可能发生在编译阶段(编译器优化)或CPU运行阶段(处理器优化)。
举个栗子🌰:做饭的步骤
假设你要做一道菜,步骤是:
- 洗锅 → 2. 热油 → 3. 放菜 → 4. 翻炒
指令重排后可能变成:
- 洗锅 → 3. 放菜(未热油) → 2. 热油 → 4. 翻炒
→ 单线程下没问题(最终菜还是熟的),但多线程下可能翻车(其他线程看到“放菜”时油还没热)!
为什么需要指令重排?
- 提高执行效率:CPU和编译器会通过重排指令,充分利用硬件资源(如并行执行不冲突的操作)。
- 减少等待时间:避免因某些操作(如内存读取延迟)导致的空闲等待。
多线程环境中的问题
指令重排在单线程无感知,但在多线程并发时可能导致意外结果。
经典案例:双重检查锁(DCL)单例模式
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题在此!
}
}
}
return instance;
}
}
问题分析:
instance = new Singleton()
实际分为三步:
- 分配内存空间
- 初始化对象
- 将引用指向内存地址
指令重排可能导致步骤2和3颠倒:
→ 其他线程可能在对象未初始化完成时,拿到非空的instance
,导致使用错误!
如何禁止指令重排?
-
使用
volatile
关键字:
→ 修饰变量(如private volatile static Singleton instance;
),通过插入内存屏障禁止重排序。
→ 解决上述DCL单例问题。 -
使用
synchronized
或Lock
:
→ 同步代码块保证原子性和可见性,隐含禁止重排序。
总结
- 指令重排:优化手段,单线程安全,多线程需警惕。
- 解决方案:
volatile
或同步机制确保多线程下的顺序一致性。
在 Java 中,volatile
、synchronized
和 Lock
是解决并发问题的三种重要工具。它们有不同的使用场景和特点,下面分别介绍它们的用途、常用方法以及适用场景。
1. volatile
用途
- 保证可见性:确保一个线程对
共享变量的修改
对其他线程立即可见。 - 防止指令重排序:通过插入内存屏障(Memory Barrier)禁止某些编译器或处理器的指令重排序优化。
特点
- 只适用于单个变量。
- 不保证复合操作(如
x++
)的原子性。 - 开销较小,性能优于
synchronized
。
常用场景
- 用于状态标志位(如开关标志)。
- 当只需要保证可见性和有序性时使用。
例子
class VolatileExample {
private volatile boolean flag = true;
public void stop() {
flag = false; // 修改flag,其他线程会立即看到
}
public void run() {
while (flag) {
// 执行任务
}
System.out.println("Thread stopped");
}
}
2. synchronized
用途
- 保证互斥性:同一时刻只有一个线程可以执行被同步保护的代码块。
- 保证可见性:当一个线程释放锁时,会将修改后的变量值刷新到主存中,其他线程获取锁时会从主存中读取最新值。
- 防止指令重排序:通过插入内存屏障确保有序性。
特点
- 可以作用于代码块或方法。
- 提供了内置锁机制,简单易用。
- 性能较低(相对
volatile
),但在复杂场景下更可靠。
常用方法/用法
-
同步方法:
- 使用
synchronized
修饰方法,锁定当前对象(即this
)。
public synchronized void increment() { count++; }
- 使用
-
同步代码块:
- 使用
synchronized
修饰代码块,指定锁对象。
public void increment() { synchronized (lock) { count++; } }
- 使用
-
静态同步方法:
- 锁定的是类对象(
Class
实例)。
public static synchronized void incrementStatic() { staticCount++; }
- 锁定的是类对象(
例子
class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++; // 线程安全
}
public synchronized int getCount() {
return count;
}
}
3. Lock(ReentrantLock)
用途
- 提供比
synchronized
更灵活的锁机制:- 支持公平锁和非公平锁。
- 支持尝试获取锁(
tryLock()
)。 - 支持可中断锁(
lockInterruptibly()
)。 - 支持超时获取锁(
tryLock(long timeout, TimeUnit unit)
)。
- 保证互斥性、可见性和有序性。
特点
- 需要手动加锁和解锁(容易忘记解锁,导致死锁)。
- 提供更多功能,但使用复杂度更高。
- 性能通常优于
synchronized
(尤其是在高竞争情况下)。
常用方法
-
核心接口:
java.util.concurrent.locks.Lock
void lock()
:获取锁,如果锁不可用则阻塞。void unlock()
:释放锁。boolean tryLock()
:尝试获取锁,成功返回true
,失败返回false
。boolean tryLock(long timeout, TimeUnit unit)
:尝试在指定时间内获取锁。void lockInterruptibly()
:获取锁,但可以响应中断。
-
实现类:
ReentrantLock
- 可重入锁,支持公平锁和非公平锁。
例子
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class LockExample {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 加锁
try {
count++; // 线程安全
} finally {
lock.unlock(); // 释放锁
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
对比总结
特性 | volatile | synchronized | Lock(ReentrantLock) |
---|---|---|---|
互斥性 | 不支持 | 支持 | 支持 |
可见性 | 支持 | 支持 | 支持 |
有序性 | 支持(禁止重排序) | 支持 | 支持 |
原子性 | 不支持复合操作 | 支持 | 支持 |
灵活性 | 低 | 中 | 高 |
性能 | 高 | 中 | 高(高竞争下优于synchronized ) |
适用场景 | 单个变量的状态标志 | 方法或代码块的同步 | 需要高级功能(如尝试锁、公平锁等) |
选择建议
- 优先使用
volatile
:- 如果只需要保证可见性和有序性,并且不涉及复合操作。
- 使用
synchronized
:- 如果需要简单的互斥性、可见性和有序性,且不需要额外的功能。
- 使用
Lock
:- 如果需要更灵活的锁机制(如尝试锁、超时锁、公平锁等)。
通过合理选择工具,可以在保证线程安全的同时,提升程序的性能和可维护性。