Java并发编程三大特性:原子性、可见性与有序性详解
在Java多线程开发中,理解内存模型的三大特性——原子性、可见性和有序性是编写正确并发程序的基础。本文将通过具体代码场景,解释这三个概念的内在含义,并提供使用volatile、synchronized及原子类库来解决实际问题的方案。
1 原子性
定义:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
我们可以从变量赋值的角度来理解。在Java中,执行以下语句时:
int i = 12;
执行线程必须先在它自己的工作内存(高速缓存)中对变量 i 进行赋值,然后再将结果写入主存。这个过程必须是不可分割的。
1.1 非原子性操作的隐患
假设为一个32位变量赋值不具备原子性,且分为两个过程:为低16位赋值,再为高16位赋值。如果在线程A写低16位后突然被中断,而线程B此时去读取该变量,就会读到错误的混合数据。
1.2 Java中的原子性操作范围
在Java内存模型中,对基本数据类型变量的读取和赋值操作是原子性操作。但组合操作并非如此。
int x = 10; // 语句1:原子操作 int y = x; // 语句2:包含读取x、赋值y两步,非原子 x++; // 语句3:包含读取x、计算+1、写回x,非原子 x = x + 1; // 语句4:同语句3,非原子
由此可见,只有简单的单步读取或赋值才是原子的。复杂的逻辑运算通常需要借助同步机制或原子类来保证原子性。
2 可见性
定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他能够立即看到修改后的值。
2.1 可见性问题演示
考虑以下跨线程场景:
// 线程1 (CPU1) i = 13; // 线程2 (CPU2) j = i;
若线程1在CPU1上运行并将 i 修改为13,但该值暂存在CPU1的高速缓存中未刷入主存。此时线程2在CPU2上读取 i,可能仍从主存读取到旧值12。这就是因缓存一致性缺失导致的可见性问题。
2.2 解决方案:volatile 关键字
Java提供了 volatile 关键字来保证共享变量的可见性。当变量被 volatile 修饰时:
- 写操作会立即刷新到主存。
- 读操作会强制从主存重新加载最新值,并 invalidate 其他工作内存的缓存副本。
3 有序性
定义:即程序执行的顺序按照代码的先后顺序执行。但在编译器优化和CPU指令重排的影响下,实际执行顺序可能与源代码不一致。
3.1 指令重排序
JVM允许编译器和处理器对指令进行重排序以提高效率。在单线程环境中,由于所有操作依赖关系确定,重排序不影响最终结果。但在多线程环境中,缺乏同步的指令重排可能导致逻辑错误。
// 线程1 context = loadContext(); // 语句1 inited = true; // 语句2
若语句1和语句2发生重排,线程2可能在 context 未初始化完成时就判断 inited == true 并尝试使用 context,导致空指针或数据异常。
3.2 保证有序性的手段
- synchronized / Lock:同一时刻只允许一个线程执行同步代码块,实质上是通过阻塞实现了程序的串行化,从而保证了有序性。
- volatile:通过添加内存屏障(Memory Barrier),禁止特定类型的指令重排序,在一定程度上保证有序性。
4 实例分析与实践
下面通过一个简单的自增案例,对比不同方案的执行效果。
public class Test {
public volatile int inc = 0;
public void increase() {
inc++; // 注意:inc++ 不具有原子性
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1)
Thread.yield();
System.out.println(test.inc); // 结果通常小于 10000
}
}
即使使用了 volatile 保证了可见性,上述代码的输出依然往往小于预期的10000。这是因为 inc++ 包含了“读-改-写”三个步骤,volatile 并不能保证这一系列操作的原子性。
4.1 修复方案一:使用 synchronized
通过加锁使操作原子化:
public synchronized void increase() {
inc++;
}
这确保了每次只有一个线程执行 inc++,最终结果必然为10000。
4.2 修复方案二:使用 Lock
显式控制锁的粒度:
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
try { inc++; } finally { lock.unlock(); }
}
4.3 修复方案三:使用 AtomicInteger
利用 CAS 算法实现的原子类,性能通常优于重型锁:
AtomicInteger inc = new AtomicInteger();
public void increase() {
inc.getAndIncrement();
}
以上就是Java并发编程中关于原子性、可见性与有序性的详细内容,更多关于Java多线程编程的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









