Java原子变量

概述

多个线程操作共享变量(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++ 看似一行简单的代码,像是一个原子操作,其实则不然,递增操作可能会三个步骤进行:

  1. 读取当前num变量的值
  2. 执行num+1
  3. 将+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 (num.compareAndSet(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的一些概念。

参考:
[1] Why do we use atomic variables instead of a volatile in Java?
[2] An Introduction to Atomic Variables in Java
[3] When to use AtomicReference in Java?
[4] Threads and Locks
[5] Compare-and-swap
[6] Understanding and Using Atomic Memory Operations

posted @ 2020-04-12 18:58  june.js  阅读(469)  评论(0编辑  收藏  举报