在并发编程中,原子性是Java内存模型定义的三个核心特性之一。它描述了一个操作或者多个操作在执行过程中,是否能被其他线程观察到中间状态。如果一个操作具备原子性,那么从其他线程的视角来看,这个操作要么还没有开始,要么已经彻底结束,绝不会看到执行到一半的结果。比如对一个int类型的变量进行赋值,在Java中通常是原子的;但像i++这样的自增,实际上包含了读取、加一、写回三个步骤,在多线程环境下就可能被交错执行,导致丢失更新。

从内存模型看Java原子操作的基础语义
Java语言规范规定,除了long和double之外的所有基本类型(boolean、byte、short、char、int、float)的读写操作都是原子的。这意味着一个线程写入一个int变量时,另一个线程读取该变量不会读到只更新了一半的位模式。对于long和double,在32位JVM上,一次赋值可能被拆成两次32位写入,因此规范允许非原子处理,但现代商用虚拟机基本都将其实现为原子操作。这种由虚拟机保障的原子性,仅限于单一变量的简单读写,不涵盖任何逻辑复合动作。
当我们讨论原子性的语义边界时,必须区分「虚拟机保障的原子性」与「程序员需要的原子性」。业务代码中往往需要「判断余额是否充足,然后扣减」这种复合动作整体不可拆分,而单纯的变量读写原子性无法覆盖此类需求。这也是为什么很多初学者认为「int赋值是原子的就线程安全了」,结果在if-then-update逻辑里依然写出了并发bug。原子操作语义的真正价值,在于为上层同步手段提供正确性基准。
下面的代码展示了非原子复合操作带来的典型问题。两个线程各执行一千次自增,理论上结果应为两千,但实际运行经常小于该值。
public class AtomicDemo {
private int count = 0;
public void increment() {
count++; // 非原子:读-改-写
}
public static void main(String[] args) throws InterruptedException {
AtomicDemo demo = new AtomicDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) demo.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) demo.increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终count=" + demo.count);
}
}
使用锁机制与原子类保证复合操作原子性
要让复合操作具备原子性,最直观的方式是使用synchronized关键字。它通过对对象监视器加锁,确保同一时刻只有一个线程能进入同步块,从而将读改写过程封装为对外不可分割的整体。这种方案语义清晰、不易出错,但在高并发且临界区较短时,线程阻塞与唤醒的上下文切换开销会比较明显。另一类方案是显式Lock接口,如ReentrantLock,它提供了更灵活的加锁中断与超时能力,但同样属于阻塞式同步。
从JDK 5开始,java.util.concurrent.atomic包提供了一系列无锁原子类,例如AtomicInteger、AtomicLong、AtomicReference。它们底层依赖处理器的CAS(Compare-And-Swap)指令,以乐观锁思路实现原子更新:线程先读取旧值,计算新值,再尝试用CAS将旧值替换为新值,若期间被其他线程改动则重试。这样可以在不阻塞线程的前提下保证复合操作的原子性,非常适合计数器、状态标志等场景。
下面是用AtomicInteger改写后的安全计数器,无论多少线程并发自增,最终结果都准确等于两千。
import java.util.concurrent.atomic.AtomicInteger;
public class SafeAtomicDemo {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子自增
}
public static void main(String[] args) throws InterruptedException {
SafeAtomicDemo demo = new SafeAtomicDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) demo.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) demo.increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终count=" + demo.count.get());
}
}
原子性与可见性、volatile的协作关系
很多开发者容易混淆原子性与可见性。原子性解决的是操作是否可被中断的问题,可见性解决的是一个线程的修改何时对其他线程可见的问题。即便某个操作是原子的,如果缺乏可见性保障,其他线程也可能一直读到旧值。volatile关键字保证了可见性与禁止指令重排序,但不保证复合操作的原子性。例如volatile int i,执行i++依然不是线程安全的,因为读和写之间仍可能被插入其他线程的操作。
在实际设计中,常常需要将原子类或锁与volatile配合使用。比如一个生产者线程通过AtomicBoolean的compareAndSet来原子地翻转状态,而消费者线程用volatile boolean变量做快速失败判断,避免频繁CAS竞争。理解三者差异有助于写出既正确又高效的并发代码:简单标志位可用volatile;复合更新用原子类或锁;跨变量的不变性约束则必须用锁或更高级的并发容器来维持整体一致性。
下面的表格总结了不同同步手段在原子性保障上的特征,便于在方案选型时快速对照。
| 手段 | 是否保证原子性 | 是否阻塞 | 适用场景 |
|---|---|---|---|
| 普通变量读写 | 基本类型单写单读是 | 否 | 无共享可变状态 |
| volatile变量 | 仅单一读写是 | 否 | 状态标志、轻量通知 |
| synchronized | 是(代码块级) | 是 | 复杂复合操作、多变量约束 |
| Atomic原子类 | 是(方法级CAS) | 否 | 计数器、无锁更新 |
综合来看,Java原子性并非一个单一语法,而是一组由内存模型、虚拟机实现与并发库共同支撑的语义保证。只有理清从基础读写原子性到复合操作原子化的路径,才能在并发程序中少踩坑、写出可维护的线程安全代码。