Java 多线程三大特性详解:原子性、可见性、有序性
——含完整代码示例与底层原理剖析
文档目标:系统掌握 Java 并发三大核心特性的实现机制、使用场景及底层原理
一、引言:为什么需要三大特性?
在多线程环境中,多个线程共享内存,若不加控制,会出现以下问题:
- 原子性破坏:
i++操作被拆分为“读-改-写”,导致结果错误; - 可见性缺失:线程 A 修改变量,线程 B 仍读取旧值;
- 有序性混乱:编译器/CPU 指令重排序导致逻辑错乱(如 DCL 单例失效)。
Java 内存模型(JMM)通过 原子性(Atomicity)、可见性(Visibility)、有序性(Ordering) 三大特性保障并发安全。
⚠️ 注意:“一致性”不是 Java 并发三大特性的标准术语,常见于数据库(ACID)或分布式系统(CAP)。
二、三大特性定义
| 特性 | 定义 | 核心问题 |
|---|---|---|
| 原子性 | 操作不可分割,要么全部执行成功,要么完全不执行 | 复合操作被线程交错打断 |
| 可见性 | 一个线程修改共享变量后,其他线程能立即看到最新值 | 工作内存与主内存不同步 |
| 有序性 | 程序执行顺序应符合代码编写顺序 | 编译器/CPU 指令重排序 |
三、原子性(Atomicity)
1. synchronized —— JVM 内置锁
✅ 代码示例
publicclassSynchronizedExample{privateint count =0;publicsynchronizedvoidincrement(){ count++;// 非原子操作,需同步保护}publicstaticvoidmain(String[] args)throwsInterruptedException{SynchronizedExample e =newSynchronizedExample();Thread t1 =newThread(()->{for(int i =0; i <1000; i++) e.increment();});Thread t2 =newThread(()->{for(int i =0; i <1000; i++) e.increment();}); t1.start(); t2.start(); t1.join(); t2.join();System.out.println("Final count: "+ e.count);// 输出 2000}}🔍 底层原理
- Monitor 机制:每个 Java 对象关联一个 Monitor(C++ 实现),由 JVM 管理。
- 对象头 Mark Word:存储锁状态(无锁/偏向锁/轻量级锁/重量级锁)。
- 字节码指令 :
monitorenter:进入同步块时获取锁;monitorexit:退出时释放锁。
- 内存语义:进入/退出同步块时自动刷新工作内存 ↔ 主内存,同时保证可见性与有序性。
锁升级路径 :
无锁 → 偏向锁(单线程优化) → 轻量级锁(自旋) → 重量级锁(OS 阻塞) 2. ReentrantLock —— 显式可重入锁
✅ 代码示例
importjava.util.concurrent.locks.ReentrantLock;publicclassReentrantLockExample{privateint count =0;privatefinalReentrantLock lock =newReentrantLock();publicvoidincrement(){ lock.lock();try{ count++;}finally{ lock.unlock();// 必须在 finally 中释放}}publicstaticvoidmain(String[] args)throwsInterruptedException{ReentrantLockExample e =newReentrantLockExample();Thread t1 =newThread(()->{for(int i =0; i <1000; i++) e.increment();});Thread t2 =newThread(()->{for(int i =0; i <1000; i++) e.increment();}); t1.start(); t2.start(); t1.join(); t2.join();System.out.println("Final count: "+ e.count);// 输出 2000}}🔍 底层原理
- AQS(AbstractQueuedSynchronizer) :
- 核心字段:
volatile int state(锁状态)、Node head/tail(CLH 双向等待队列)。
- 核心字段:
- 加锁流程:
- CAS 尝试将
state从 0 → 1; - 失败则封装线程为
Node入队,并调用LockSupport.park()阻塞;
- CAS 尝试将
- 解锁流程 :
- CAS 将
state减 1; - 唤醒队列头节点(
LockSupport.unpark())。
- CAS 将
- 可重入性:记录当前持有线程,同一线程可多次加锁(
state递增)。 - 优势:支持公平锁、超时、可中断、多条件变量(
Condition)。
3. 原子类(如 AtomicInteger)
✅ 代码示例
importjava.util.concurrent.atomic.AtomicInteger;publicclassAtomicExample{privateAtomicInteger count =newAtomicInteger(0);publicvoidincrement(){ count.incrementAndGet();// 原子自增}publicstaticvoidmain(String[] args)throwsInterruptedException{AtomicExample e =newAtomicExample();Thread t1 =newThread(()->{for(int i =0; i <1000; i++) e.increment();});Thread t2 =newThread(()->{for(int i =0; i <1000; i++) e.increment();}); t1.start(); t2.start(); t1.join(); t2.join();System.out.println("Final count: "+ e.count.get());// 输出 2000}}🔍 底层原理
- Unsafe 类:调用 native 方法
compareAndSwapInt(),利用 CPU 原子指令(如 x86 的LOCK CMPXCHG)。 - 无锁并发:失败则自旋重试,避免线程阻塞开销。
- volatile 修饰:内部字段
private volatile int value,天然具备可见性与部分有序性。 - ABA 问题:可通过
AtomicStampedReference解决。
CAS(Compare-And-Swap) :
publicfinalintincrementAndGet(){return unsafe.getAndAddInt(this, valueOffset,1)+1;}4. StampedLock —— 乐观读写锁
✅ 代码示例
importjava.util.concurrent.locks.StampedLock;publicclassStampedLockExample{privatedouble x =0, y =0;privatefinalStampedLock sl =newStampedLock();voidmove(double dx,double dy){long stamp = sl.writeLock();try{ x += dx; y += dy;}finally{ sl.unlockWrite(stamp);}}doubledistanceFromOrigin(){long stamp = sl.tryOptimisticRead();// 乐观读double currentX = x, currentY = y;if(!sl.validate(stamp)){// 检查是否被写入 stamp = sl.readLock();// 升级为悲观读try{ currentX = x; currentY = y;}finally{ sl.unlockRead(stamp);}}returnMath.sqrt(currentX * currentX + currentY * currentY);}publicstaticvoidmain(String[] args){StampedLockExample obj =newStampedLockExample(); obj.move(3,4);System.out.println("Distance: "+ obj.distanceFromOrigin());// 输出 5.0}}🔍 底层原理
- 改进型 AQS:使用
long stamp代替int state,低 7 位表示锁模式,高位为版本号。 - 乐观读 :
- 不加锁,仅记录版本号;
- 读取后验证版本是否变化(
validate(stamp)); - 若未被写入,则读成功;否则升级为悲观读。
- 写锁与读锁互斥,但多个乐观读可并发。
- 不支持重入,且无条件变量。
- 适用场景:读远多于写的高性能计算(如几何坐标计算)。
四、可见性(Visibility)
1. volatile —— 轻量级可见性保障
✅ 代码示例
publicclassVolatileVisibilityExample{privatevolatileboolean running =true;// volatile 保证可见性publicvoidstop(){ running =false;}publicvoidrunTask(){while(running){System.out.println("Working...");try{Thread.sleep(500);}catch(InterruptedException e){break;}}System.out.println("Task stopped.");}publicstaticvoidmain(String[] args)throwsInterruptedException{VolatileVisibilityExample task =newVolatileVisibilityExample();Thread worker =newThread(task::runTask); worker.start();Thread.sleep(2000); task.stop();// 主线程停止任务 worker.join();}}❗ 若去掉volatile,worker线程可能永远看不到running = false,导致死循环。
🔍 底层原理
- JMM 内存语义:
- 写 volatile:立即刷回主内存,并使其他 CPU 缓存行失效;
- 读 volatile:从主内存重新加载最新值。
- 内存屏障(Memory Barrier):
- 写屏障:
StoreStore+StoreLoad,禁止写之前的操作重排到写之后; - 读屏障:
LoadLoad+LoadStore,禁止读之后的操作重排到读之前。
- 写屏障:
- 缓存一致性协议:如 MESI 协议,确保多核 CPU 缓存一致性。
- 不保证原子性:如
i++仍需锁或原子类。
2. synchronized 保证可见性
✅ 代码示例(复用前文 SynchronizedExample)
🔍 底层原理
- happens-before 规则:解锁 happens-before 后续加锁。
- 内存同步:
- 进入同步块:清空工作内存,从主内存重新加载共享变量;
- 退出同步块:将修改后的共享变量 flush 到主内存。
- 底层通过 Monitor 的 entry/exit 操作触发内存同步。
3. final 字段的可见性
✅ 代码示例
publicclassFinalFieldExample{privatefinalint x;privatefinalint y;publicFinalFieldExample(int x,int y){this.x = x;this.y = y;// 构造完成后对所有线程可见}publicintsum(){return x + y;// 无需同步,安全}publicstaticvoidmain(String[] args){FinalFieldExample obj =newFinalFieldExample(3,4);newThread(()->{System.out.println("Sum in thread: "+ obj.sum());// 输出 7}).start();}}🔍 底层原理
- JLS §17.5 final 语义:
- 构造函数内对 final 字段的写操作,不会被重排到构造函数之外;
- 构造完成后,final 字段的值对所有线程立即可见。
- 安全发布:只要对象正确构造(未逸出),final 字段无需同步即可安全读取。
- 适用场景:构建不可变对象(Immutable Object)。
五、有序性(Ordering)
1. volatile 禁止重排序(DCL 单例)
✅ 代码示例
publicclassDoubleCheckedLockingSingleton{// 必须加 volatile!否则可能返回未完全初始化的对象privatevolatilestaticDoubleCheckedLockingSingleton instance;privateDoubleCheckedLockingSingleton(){}publicstaticDoubleCheckedLockingSingletongetInstance(){if(instance ==null){synchronized(DoubleCheckedLockingSingleton.class){if(instance ==null){ instance =newDoubleCheckedLockingSingleton();// 关键:防止重排序}}}return instance;}publicvoiddoSomething(){System.out.println("Singleton working...");}publicstaticvoidmain(String[] args){DoubleCheckedLockingSingleton s1 =getInstance();DoubleCheckedLockingSingleton s2 =getInstance();System.out.println(s1 == s2);// true s1.doSomething();}}🔍 底层原理
- 对象创建三步骤:
- 分配内存空间;
- 初始化对象;
- 将
instance指向内存地址。
- 重排序风险:步骤 2 与 3 可能重排为
1 → 3 → 2,导致其他线程拿到未初始化对象。 - volatile 作用:
- 在
instance = new ...写操作后插入 StoreStore 屏障; - 确保初始化完成后再赋值引用。
- 在
- happens-before:volatile 写 happens-before 后续 volatile 读。
2. synchronized 保证有序性
✅ 代码示例(复用前文)
🔍 底层原理
- 程序顺序规则:同步块内部代码按程序顺序执行。
- 监视器锁规则:解锁 happens-before 后续加锁,建立跨线程顺序。
- 底层 Monitor 机制隐含内存屏障,禁止临界区内外的操作重排。
六、综合对比表
| 方案 | 原子性 | 可见性 | 有序性 | 底层机制 | 适用场景 |
|---|---|---|---|---|---|
synchronized | ✅ | ✅ | ✅ | Monitor(对象头 Mark Word) | 通用同步,复合操作 |
ReentrantLock | ✅ | ✅ | ✅ | AQS + CAS + CLH 队列 | 需要高级控制(超时、公平等) |
| 原子类 | ✅(单变量) | ✅ | ✅ | CAS + volatile + Unsafe | 单变量无锁并发 |
StampedLock | ✅ | ✅ | ✅ | 改进 AQS + 乐观读 | 读多写少高性能场景 |
volatile | ❌ | ✅ | ✅(部分) | 内存屏障 + 缓存一致性协议 | 状态标志、DCL 单例 |
final | ❌ | ✅(构造后) | ✅(构造阶段) | JMM final 语义 | 不可变对象 |
七、最佳实践建议
- 状态标志 →
volatile boolean flag - 计数器/累加器 →
AtomicInteger或LongAdder(高并发分段累加) - 复合操作(如转账) →
synchronized(简单)或ReentrantLock(灵活) - 高性能读多写少 →
StampedLock - 不可变对象 →
final字段 + 构造函数安全初始化 - 避免过度同步:锁粒度越小越好,优先无锁方案
八、附录:关键概念速查
| 概念 | 说明 |
|---|---|
| JMM(Java Memory Model) | 定义线程与主内存交互规则 |
| happens-before | JMM 核心规则,保证操作可见性与有序性 |
| CAS(Compare-And-Swap) | CPU 原子指令,无锁并发基础 |
| AQS | JUC 锁框架基石,基于 CLH 队列 |
| 内存屏障 | 禁止指令重排序的硬件/编译器指令 |
💡 口诀记忆:原(原子性)可(可见性)有(有序性)——Java 并发三基石。
文档版本:v1.0
最后更新:2026年1月19日
作者:chen