概述
多个线程操作共享变量(Java堆内存上的数据)会带来bug,Java提供了锁机制(Lock)来管理多线程并发,比如synchronized,但是会带来额外的性能开销(线程阻塞,上下文切换等)。为了提升性能,Java引入了原子变量,通过无锁算法(lock-free)实现多线程安全,比如CAS。
原子变量只是实现多线程安全的一个手段,在对单个共享变量进行”读取-修改-写入“操作的场景下很适合,所以,其适用场景没有synchronized广泛。
多线程问题
首先,实现一个计数器,代码如下:
public class Counter {
private volatile int num;
public void increment() {
num++;
}
public static void main(String[] args) {
Counter counter = new Counter();
// 多线程递增计数器
IntStream.range(0, 100).parallel().forEach(i -> counter.increment());
// 打印结果
System.out.println("counter: " + counter.num);
}
}
多次运行上述代码,打印出来的值不是100,而是98,97等。
这是一个典型的多线程问题,num++ 看似一行简单的代码,像是一个原子操作,其实则不然,递增操作可能会三个步骤进行:
读取当前num变量的值
执行num+1
将+1后的值赋值给num变量
所以,多个线程更新后的值会出现覆盖的情况,比如两个线程同时拿到了num的值为50,在各自的线程中执行加法操作后为51,然后更新主存中的值为51,但是我们期望的值是52。
通过synchronized解决
给increment()方法增加synchronized关键字,如下:
// ...
public synchronized void increment() {
num++;
}
// ...
synchronized是Java中最常用的锁,保证被“监控”代码块在同一个时刻只能由一个线程执行,所以最终出来的结果为100,正确。
但是,该方法会导致没获取锁的线程挂起,发生上下文切换,这就是重量级锁带来的性能开销。
通过原子变量AtomicInteger解决
atomic包下有AtomicInteger类,可以解决上述问题,代码如下:
public class Counter {
private AtomicInteger num = new AtomicInteger(0);
public void increment() {
while (true) {
int oldValue = num.get();
int newValue = oldValue + 1;
if (pareAndSet(oldValue, newValue)) {
return;
}
}
}
public static void main(String[] args) {
Counter counter = new Counter();
// 多线程递增计数器
IntStream.range(0, 100).parallel().forEach(i -> counter.increment());
// 打印结果
System.out.println("counter: " + counter.num);
}
}
运行代码,输出结果100。
CAS原子操作
Java并发包下的原子变量利用了CAS机制,实现了原子操作。这儿说的原子操作,是指CPU对某一块内存的原子操作(Atomic memory operation),具备如下特点:
串行化多个线程对同一块内存的更新操作(保证多线程更新数据时的安全)。
读取-修改-写入这三个操作不可被中断,更新操作要不然成功,要不然失败,不会出现中间状态(保证数据完整性)。
只有当内存中的值与期望值相同时,才会执行更新操作(保证正确的逻辑)。
在并发编程中,CAS属于”乐观锁“,假设多线程竞争几率很小,或者在很短的时间内竞争状态会结束,如果多线程竞争非常频繁,会使CPU长时间空转(busy waiting),造成资源浪费。所以,没有银弹!根据场景选择技术方案。
CAS(Compare And Swap)需要特定的CPU指令支持,所以并不是所有硬件平台都支持CAS。Java跨平台的特性要求API的行为一致性,所以在不支持CAS的硬件平台上,atomic会退化成重量级锁。
总结
实现多线程的手段很多,根据场景选择合理的技术方案可以提升程序的性能。本文简单讲述了Java中原子变量是如何解决多线程问题,以及CAS的一些概念。