一、Java内存模型(JMM)概述
Java内存模型(Java Memory Model, JMM)是Java虚拟机规范中定义的一种抽象模型,用于描述线程如何与内存交互,以及在多线程环境下如何解决原子性、可见性和有序性问题。JMM的核心目标是屏蔽硬件和操作系统的内存访问差异,为开发者提供统一的多线程编程规则。
二、JMM的核心概念
- 主内存(Main Memory)
- 所有线程共享的内存区域,存储所有变量(实例字段、静态字段、数组元素等)。
- 类似于物理机的主存(RAM),是线程间共享数据的唯一来源。
- 工作内存(Working Memory)
- 每个线程私有的内存区域,包含线程对共享变量的副本。
- 线程对变量的操作(读取、赋值)必须在工作内存中完成,之后才会刷新到主内存。
- 内存交互操作
JMM定义了8种原子操作,保证线程与内存的交互规则:
- lock/unlock:锁定/解锁变量,标识为独占状态。
- read/load:将变量从主内存读取到工作内存。
- use/assign:将变量传递给执行引擎或赋值。
- store/write:将工作内存的变量写回主内存。
三、JMM的三大特性
- 原子性(Atomicity)
- 保证基本数据类型的读写是原子的(如
int
、boolean
等),但复合操作(如i++
)需要同步机制。 - 实现方式:
synchronized
、ReentrantLock
、CAS
(Compare and Swap)操作(如AtomicInteger
)。
- 可见性(Visibility)
- 当一个线程修改共享变量后,其他线程能立即看到最新值。
- 实现方式:
volatile
关键字、synchronized
、Lock
的释放操作。
- 有序性(Ordering)
- 程序执行顺序与代码顺序一致,但编译器和处理器可能进行指令重排序。
- 实现方式:
volatile
禁止重排序、synchronized
的内存屏障(Memory Barrier)。
四、JMM的关键工具
- volatile关键字
- 作用:保证变量的可见性和禁止指令重排序。
- 限制:不保证原子性(如
volatile int count = 0; count++;
仍需加锁)。 - 典型场景:
- 双重检查单例模式(防止指令重排序):
- 双重检查单例模式(防止指令重排序):
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // volatile防止未初始化对象被返回
}
}
}
return instance;
}
}
- synchronized关键字
- 作用:保证代码块或方法的原子性、可见性和有序性。
- 实现原理:
- 加锁(
monitorenter
):将工作内存的变量失效,重新从主内存加载。 - 解锁(
monitorexit
):将工作内存的变量刷新到主内存。
- 加锁(
- 锁升级机制:
- 偏向锁:单线程访问时,记录线程ID,减少CAS操作。
- 轻量级锁:多线程交替访问时,通过自旋尝试获取锁。
- 重量级锁:高竞争时,依赖操作系统互斥量(Mutex),线程挂起。
- 显式锁(ReentrantLock)
- 提供与
synchronized
类似的功能,但支持更灵活的锁操作(如尝试获取锁、超时、公平锁等)。 - 优点:支持响应中断、可轮询、可定时、可绑定多个条件。
- final关键字
- 作用:保证不可变对象的安全发布。
- 内存语义:构造函数中对
final
字段的写入不会被重排序到构造函数外。
五、happens-before原则
JMM通过happens-before关系定义操作之间的可见性和顺序性。以下是主要规则:
- 程序顺序原则:同一个线程中,代码按顺序执行。
- 锁规则:解锁操作先于同一锁的加锁操作。
- volatile规则:写
volatile
变量先于读该变量。 - 线程启动规则:
Thread.start()
先于线程中的任何操作。 - 线程终止规则:
Thread.join()
先于线程中所有操作结果。 - 传递性:若A happens-before B,B happens-before C,则A happens-before C。
示例:
// 线程1
sharedVar = 10; // A
flag = true; // B
// 线程2
while (!flag) {} // C
System.out.println(sharedVar); // D
- 根据程序顺序原则,A happens-before B,C happens-before D。
- 根据volatile规则(假设
flag
是volatile
),B happens-before C。 - 最终,A happens-before D,保证
sharedVar
的值为10。
六、锁策略与并发工具
- 乐观锁 vs 悲观锁
- 乐观锁:假设冲突少,通过版本号(CAS)检测冲突(如
AtomicInteger
)。 - 悲观锁:假设冲突多,直接加锁(如
synchronized
)。
- 自旋锁
- 在轻量级锁中,线程通过循环(自旋)尝试获取锁,避免阻塞开销。
- 读写锁(Read-Write Lock)
- 允许多线程并发读,但写操作互斥(如
ReentrantReadWriteLock
)。
- 分段锁
- 将共享资源拆分为多个段,降低锁竞争(如
ConcurrentHashMap
)。
- CAS操作
- 无锁编程的核心,通过
Compare and Swap
实现原子操作(如AtomicInteger.incrementAndGet()
)。
七、常见问题与解决方案
- 内存可见性问题
- 场景:线程A修改变量后,线程B无法立即看到。
- 解决方案:使用
volatile
或同步机制(如synchronized
)。
- 指令重排序问题
- 场景:单例模式中返回未完全初始化的对象。
- 解决方案:用
volatile
修饰单例实例。
- 原子性问题
- 场景:多线程递增操作导致结果错误。
- 解决方案:使用原子类(如
AtomicInteger
)或同步块。
八、最佳实践
- 优先使用并发工具类
- 使用
java.util.concurrent
包中的工具(如ConcurrentHashMap
、CountDownLatch
)。
- 最小化锁范围
- 缩小锁的粒度,减少锁竞争。
- 避免死锁
- 按固定顺序加锁,或使用
tryLock
。
- 合理使用volatile
- 仅在需要可见性和禁止重排序时使用,避免过度依赖。
九、总结
Java内存模型(JMM)是多线程编程的基石,通过定义主内存与工作内存的交互规则,解决了多线程环境下的原子性、可见性和有序性问题。结合锁机制(如synchronized
、ReentrantLock
)和并发工具(如volatile
、CAS
),开发者可以编写高效且线程安全的代码。理解JMM和锁机制的底层原理,是掌握Java并发编程的关键。
© 版权声明
本站资源来自互联网收集,仅供用于学习和交流,请勿用于商业用途。如有侵权、不妥之处,请联系站长并出示版权证明以便删除。敬请谅解!
THE END