1. 简介
简单来说,当涉及并发时,共享可变状态极易引发问题。如果对共享可变对象的访问管理不当,应用程序很快就会陷入难以检测的并发错误。
本文将回顾使用锁处理并发访问的传统方式,探讨锁机制存在的缺点,最后介绍原子变量作为替代方案。
2. 锁机制
先看这个类:
public class Counter {
int counter;
public void increment() {
counter++;
}
}
在单线程环境下,这段代码完美运行;但一旦允许多个线程写入,结果就会变得不一致。
这是因为看似原子的 counter++
操作,实际由三个步骤组成:获取值、递增、写回新值。
如果两个线程同时尝试获取并更新值,可能导致更新丢失。
管理对象访问的常见方式是使用锁。通过在 increment
方法签名上添加 synchronized
关键字实现(关于锁与同步的更多细节可参考:Java Synchronized 关键字指南):
public class SafeCounterWithLock {
private int counter;
public synchronized void increment() {
counter++;
}
}
使用锁确实解决了问题,但性能会受损。
当多个线程尝试获取锁时,只有一个线程能成功,其余线程会被阻塞或挂起。
线程挂起和恢复的过程开销极大,严重影响系统整体效率。
在像计数器这样的小程序中,上下文切换耗时可能远超实际代码执行时间,导致整体效率大幅下降。
3. 原子操作
并发编程领域有一个研究方向专注于创建非阻塞算法。这些算法利用底层原子机器指令(如比较并交换 CAS)来保证数据完整性。
典型的 CAS 操作包含三个操作数:
- 操作的内存位置(M)
- 变量的当前预期值(A)
- 需要设置的新值(B)
CAS 操作会原子性地将 M 的值更新为 B,但前提是 M 的当前值必须等于 A,否则不执行任何操作。
无论是否成功,都会返回 M 的当前值。这相当于将"获取值-比较值-更新值"三个步骤合并为单个机器级指令。
当多个线程通过 CAS 尝试更新同一值时,只有一个线程会成功并更新值。但与锁机制不同,其他线程不会被挂起;它们只是被告知更新失败。线程可以继续执行后续任务,完全避免了上下文切换。
另一个副作用是核心程序逻辑变得更复杂。因为我们需要处理 CAS 操作失败的情况:可以不断重试直到成功,也可以根据业务场景选择放弃操作。
4. Java 中的原子变量
Java 中最常用的原子变量类包括:
AtomicInteger
AtomicLong
AtomicBoolean
AtomicReference
它们分别表示可原子更新的 int
、long
、boolean
和对象引用。这些类暴露的核心方法有:
-
get()
– 从内存获取值,确保其他线程的修改可见;等效于读取volatile
变量 -
incrementAndGet()
– 原子性地将当前值加一 -
set()
– 将值写入内存,确保对其他线程可见;等效于写入volatile
变量 -
lazySet()
– 最终将值写入内存,可能与后续相关内存操作重排序。典型场景是:为垃圾回收而置空不再访问的引用。通过延迟volatile
写入可获得更好性能 -
compareAndSet()
– 如第 3 节所述,成功返回true
,否则返回false
-
weakCompareAndSet()
– 如第 3 节所述,但内存语义更弱:不建立 happens-before 顺序,可能看不到其他变量的更新。自 Java 9 起,该方法在所有原子实现中已废弃,改用weakCompareAndSetPlain()
。原方法内存效果是 plain(普通)但名称暗示 volatile 效果,为避免混淆,废弃该方法并新增了四种不同内存效果的方法,如weakCompareAndSetPlain()
或weakCompareAndSetVolatile()
使用 AtomicInteger
实现的线程安全计数器示例如下:
public class SafeCounterWithoutLock {
private final AtomicInteger counter = new AtomicInteger(0);
int getValue() {
return counter.get();
}
void increment() {
counter.incrementAndGet();
}
}
可以看到,incrementAndGet()
方法相当于一个同步代码块:获取当前值、加一、赋值给计数器变量,最后存储到内存中。
5. 总结
本教程介绍了处理并发的替代方案,可避免锁机制带来的性能问题。我们还探讨了 Java 原子变量类暴露的核心方法。
所有示例代码均可在 GitHub 获取。
要了解更多内部使用非阻塞算法的类,可参考 ConcurrentMap 指南。