除了 synchronized 关键字,Java 中还有多种方式可以保证多线程环境下的有序性,核心原理依然围绕 禁止指令重排序 和 建立 happens-before 关系,具体包括以下几类常用方案,附原理、使用场景和注意事项:
一、volatile 关键字(轻量级有序性保证)
1. 原理
volatile 是 JMM 提供的轻量级同步机制,通过 插入内存屏障 禁止指令重排序,同时保证可见性(不保证原子性)。
- 写操作后插入
StoreStore+StoreLoad屏障:确保 volatile 变量的写操作先于后续所有写 / 读操作执行,且修改立即写回主内存。 - 读操作前插入
LoadLoad+LoadStore屏障:确保 volatile 变量的读操作后于所有读 / 写操作执行,且直接从主内存加载最新值。 - 遵循
happens-before的 "volatile 变量规则":对 volatile 变量的写操作happens-before后续的读操作。
2. 使用场景
适合 状态标志位、单例模式双重检查锁 等场景,用于保证 "写操作" 与 "读操作" 的顺序一致性。
3. 示例代码
java
运行
csharp
// 状态标志位:保证线程A的 flag=true 先于线程B的 flag 判断执行
public class VolatileOrderExample {
private volatile boolean flag = false;
private int value = 0;
// 线程A执行
public void writer() {
value = 100; // 普通变量写
flag = true; // volatile变量写(禁止与上一行重排序)
}
// 线程B执行
public void reader() {
if (flag) { // volatile变量读(禁止与下一行重排序)
System.out.println(value); // 必然输出100,而非0
}
}
}
4. 注意事项
- 不保证原子性:
volatile int i = 0; i++仍可能出现并发问题(需配合Atomic类或锁)。 - 仅对 "volatile 变量本身的读写" 与其他指令的重排序进行限制,不能保证普通变量的跨线程有序性(需依赖
happens-before传递性)。
二、final 关键字(初始化阶段有序性保证)
1. 原理
final 关键字通过 禁止重排序初始化过程 保证有序性,核心规则:
- 对于
final修饰的基本类型变量:编译器确保变量初始化完成后,才能被其他线程访问(禁止 "初始化" 与 "访问" 重排序)。 - 对于
final修饰的引用类型变量:编译器确保引用指向的对象完全初始化后,才能将引用赋值给final变量(禁止 "对象初始化" 与 "引用赋值" 重排序)。 - 遵循
happens-before规则:final变量的初始化完成happens-before任何线程对该变量的访问。
2. 使用场景
适合 不可变对象、单例模式的实例变量 等场景,避免出现 "半初始化" 对象。
3. 示例代码
java
运行
arduino
// final 保证对象初始化有序性,避免线程访问到半初始化的对象
public class FinalOrderExample {
private final int basicVal; // final 基本类型
private final User user; // final 引用类型
// 构造方法:初始化必须在构造器内完成
public FinalOrderExample() {
basicVal = 10; // 基本类型初始化
user = new User("张三"); // 引用类型初始化(对象完全创建后才赋值给 user)
}
// 其他线程访问时,basicVal 和 user 必然已完全初始化
public int getBasicVal() { return basicVal; }
public User getUser() { return user; }
}
class User {
private String name;
public User(String name) { this.name = name; }
}
4. 注意事项
final变量必须在构造器内初始化或声明时赋值(否则编译报错)。- 若
final引用指向的对象内部成员可变(如User的name可修改),则对象内部的有序性仍需其他机制保证(如synchronized)。
三、java.util.concurrent.locks.Lock 接口(灵活的锁机制)
1. 原理
Lock 是 JDK 1.5 引入的并发工具,提供比 synchronized 更灵活的锁控制,其有序性保证逻辑与 synchronized 类似,但支持更多特性:
- 互斥执行:同一时间只有一个线程能获取锁,确保同步块内的操作有序执行。
happens-before关系 :锁的释放操作happens-before后续的锁获取操作(与synchronized的 "监视器锁规则" 一致)。- 支持 公平锁 / 非公平锁、超时等待、中断等待 等,可更精细地控制有序性。
2. 核心实现类:ReentrantLock(可重入锁)
最常用的 Lock 实现,支持可重入(同一线程可多次获取锁),有序性保证与 synchronized 等价,但性能在高并发场景下更优(锁竞争激烈时)。
3. 使用场景
适合 高并发、需要灵活锁控制 的场景(如超时等待、中断、公平锁需求)。
4. 示例代码
java
运行
csharp
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// ReentrantLock 保证有序性,支持超时和中断
public class LockOrderExample {
private final Lock lock = new ReentrantLock(); // 非公平锁(默认)
private int value = 0;
public void update() {
lock.lock(); // 获取锁(可中断:lock.lockInterruptibly())
try {
value = 100; // 临界区操作,有序执行
} finally {
lock.unlock(); // 释放锁(必须在 finally 中执行,避免锁泄露)
}
}
public int getValue() {
lock.lock();
try {
return value; // 确保读取到最新的有序值
} finally {
lock.unlock();
}
}
}
5. 注意事项
- 必须在
finally块中释放锁(否则线程异常时会导致锁泄露)。 - 支持公平锁(通过
new ReentrantLock(true)创建),但公平锁性能较低(需排队获取锁),非特殊场景不推荐。 - 可通过
tryLock(long timeout, TimeUnit unit)实现超时等待,避免死锁。
四、并发工具类(基于 Lock/volatile 封装)
Java 并发包(java.util.concurrent)提供了多个封装好的工具类,内部通过 Lock、volatile 或 Unsafe 机制保证有序性,无需手动处理锁细节:
1. Atomic 原子类(java.util.concurrent.atomic)
-
原理 :基于 CAS(Compare-And-Swap)操作 +
volatile关键字,保证 "读取 - 修改 - 写入" 的原子性和有序性。 -
场景:适合单个变量的原子操作(如计数器、状态标记)。
-
示例:
java
运行
javaimport java.util.concurrent.atomic.AtomicInteger; public class AtomicOrderExample { private final AtomicInteger count = new AtomicInteger(0); // 原子自增:保证有序性和原子性 public void increment() { count.incrementAndGet(); // 底层:CAS + volatile 禁止重排序 } public int getCount() { return count.get(); // volatile 保证可见性和有序性 } }
2. CountDownLatch/CyclicBarrier(线程协作工具)
-
原理 :基于
Lock和Condition实现,通过线程间的协作等待,保证特定操作的执行顺序(如 "所有线程准备完成后再执行主线程")。 -
场景:多线程协作场景(如初始化任务完成后再执行业务逻辑)。
-
示例(CountDownLatch) :
java
运行
arduinoimport java.util.concurrent.CountDownLatch; // 保证:所有子线程执行完后,主线程才继续执行 public class CountDownLatchExample { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(2); // 2个线程需要等待 // 子线程1 new Thread(() -> { System.out.println("子线程1执行完成"); latch.countDown(); // 计数减1 }).start(); // 子线程2 new Thread(() -> { System.out.println("子线程2执行完成"); latch.countDown(); // 计数减1 }).start(); latch.await(); // 主线程等待,直到计数为0(保证子线程1、2先执行) System.out.println("主线程继续执行"); } }
3. ConcurrentHashMap 等并发集合
- 原理:基于分段锁(JDK 1.7)或 CAS + synchronized(JDK 1.8),保证多线程读写时的有序性和安全性。
- 场景:高并发下的键值对存储,无需手动同步。
五、happens-before 其他规则(隐式有序性保证)
除了上述显式机制,JMM 的 happens-before 其他规则也能隐式保证有序性,无需额外代码:
- 程序顺序规则 :单线程内,前序操作
happens-before后续操作(单线程天然有序)。 - 线程启动规则 :
Thread.start()happens-before线程内的所有操作(启动后线程可见主线程之前的修改)。 - 线程终止规则 :线程内的所有操作
happens-before线程终止检测(如Thread.join())。 - 传递性规则 :若 A
happens-beforeB,Bhappens-beforeC,则 Ahappens-beforeC。
示例(线程启动规则)
java
运行
arduino
public class ThreadStartOrderExample {
private static int value = 0;
public static void main(String[] args) {
value = 100; // 操作A
Thread thread = new Thread(() -> {
System.out.println(value); // 操作B,必然输出100
});
thread.start(); // 操作C:A happens-before C,C happens-before B → A happens-before B
}
}
六、总结:不同方案的选择建议
| 方案 | 核心优势 | 适用场景 | 注意事项 |
|---|---|---|---|
volatile |
轻量级、无锁开销 | 状态标志位、单例双重检查锁 | 不保证原子性,仅控制自身读写的重排序 |
final |
编译期约束、无运行时开销 | 不可变对象、单例实例变量 | 仅保证初始化阶段有序,对象内部可变需额外控制 |
Lock(ReentrantLock) |
灵活(超时、中断、公平锁) | 高并发、需要精细锁控制的场景 | 必须手动释放锁,避免泄露 |
Atomic 类 |
原子操作、无锁(CAS) | 单个变量的原子读写(计数器、状态) | 高并发下 CAS 自旋可能消耗 CPU |
| 并发工具类(CountDownLatch) | 线程协作、无需手动锁 | 多线程同步等待场景(如初始化、任务拆分) | 需注意计数准确性,避免死等 |
happens-before 隐式规则 |
无额外代码开销 | 单线程、线程启动 / 终止等简单协作场景 | 需理解规则,避免依赖物理执行顺序 |
核心原则:
- 简单场景(状态标志、单变量)优先用
volatile或Atomic类(无锁,性能优); - 复杂临界区(多变量操作、原子性需求)用
synchronized或ReentrantLock(互斥执行); - 线程协作场景用
CountDownLatch/CyclicBarrier(封装好的协作机制); - 不可变对象用
final(编译期保证初始化有序)。