在Java内存模型(Java Memory Model, JMM)中,“可见性”、“原子性”和“有序性”是确保多线程程序正确执行的三个核心概念。它们直接影响到多线程环境下数据的一致性和程序的行为。
可见性(Visibility)
概念: 可见性指的是当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。在多线程环境中,由于每个线程可能有自己的工作内存(本地缓存),因此并非所有的修改都会立即反映到主内存中,这可能导致其他线程看到的是过时的值。
影响: 缺乏可见性会导致数据不一致,比如一个线程更新了某个变量,但其他线程无法立即看到这个更新,可能会基于旧值做出错误的计算或决策。
解决方案:
- 使用
volatile
关键字修饰变量,确保每次读取都从主内存中读取最新值,每次写入都立即刷回到主内存。 - 使用
synchronized
关键字加锁,无论是同步方法还是同步代码块,都能确保在解锁之前,对变量的修改对其他线程可见。 java.util.concurrent
包下的原子类,如AtomicInteger
,通过CAS操作保证了修改的可见性。
代码案例:
// 使用volatile保证可见性
private volatile int counter = 0;
// 使用synchronized保证可见性
private int counter;
public synchronized void increment() {
counter++;
}
原子性(Atomicity)
概念: 原子性指的是一个操作或者一系列操作,要么全部执行并且执行过程不会被任何因素打断,要么就都不执行。在多线程环境下,非原子操作可能会被其他线程打断,导致数据处于不一致状态。
影响: 非原子操作在多线程环境下可能导致数据竞争和race condition,例如经典的读改写问题。
解决方案:
- 使用
java.util.concurrent.atomic
包下的原子类,如AtomicInteger
,提供原子的增加、减少等操作。 - 使用
synchronized
关键字锁定代码块或方法,确保同一时间只有一个线程可以执行该段代码。 - 使用
Lock
接口及其实现类,如ReentrantLock
,同样可以实现同步控制,保证操作的原子性。
代码案例:
// 使用AtomicInteger保证原子性
private AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
// 使用synchronized保证原子性
private int counter = 0;
public synchronized void increment() {
counter++;
}
有序性(Ordering)
概念: 有序性指的是程序执行的顺序按照代码的逻辑顺序执行。在JVM中,为了优化性能,编译器和处理器可能会对指令进行重排序,这在单线程环境下没问题,但在多线程环境下可能导致问题。
影响: 指令重排序可能导致多线程程序出现预期之外的行为,特别是当操作之间存在依赖关系时。
解决方案:
- 使用
synchronized
关键字,不仅提供了互斥访问,还隐含了内存屏障,确保了某些规则下的有序性。 - 使用
volatile
关键字,除了保证可见性外,还禁止特定类型的发生在volatile变量之前的读写重排序。 java.util.concurrent
包中的一些高级并发工具,如CountDownLatch
、CyclicBarrier
,也可以间接帮助维持操作的顺序。
代码案例:
// 使用volatile防止指令重排序
private volatile boolean flag = false;
public void before() {
value = 1; // 操作A
flag = true; // 操作B
}
public void after() {
if (flag) { // 读取flag的值
// 这里能确保value已经为1,因为flag为volatile,禁止了B和A的重排序
int i = value;
}
}
综上所述,理解并正确应用这些概念是编写正确且高效多线程程序的关键。