本章主要讲解原子变量和volatile关键字的使用方式和使用场景,包括Java8中新加入的LongAdder和LongAccumulator。想要了解原子变量和volatile关键字,就必须先了解Java内存模型中的原子性
和可见性
。
可见性和原子性
原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
1 | x = 10; //语句1 |
上面的语句中只有语句1是具备原子性的。也就是说其他三种语句在多线程环境下会出错。
我们想要让其他的三种语句都变成原子操作该怎么做?这时可以用到之前介绍的synchronized和锁,还有下面要介绍原子变量。
可见性
可见性就是当一个变量被一个修改时,它的值会在主存中立即刷新,因此其他的所有线程都会主存中看到它的新值。而Java模型本身是不保证可见性的,也就是说上面的语句1,2,3,4本身都不具备可见性。
我们想要让上面4种语句都具有可见性该怎么做?同样可以用到之前介绍的synchronized和锁,还有下面要介绍volatile关键字。
原子变量
AtomicInteger
java.concurrent.atomic
包包含了许多实用的类,用于执行原子操作。如果你能够在多线程中同时且安全地执行某个操作,而不需要synchronized
关键字或锁,那么这个操作就是原子的。
本质上,原子操作严重依赖于比较与交换(CAS),它是由多数现代CPU直接支持的原子指令。这些指令通常比同步块要快。所以在只需要并发修改单个可变变量的情况下,我建议你优先使用原子类,而不是锁。
对于其它语言,一些语言的原子操作用锁实现,而不是原子指令。
现在让我们选取一个原子类,例如AtomicInteger
:
1 | private static AtomicInteger atomicInt = new AtomicInteger(0); |
通过使用AtomicInteger
代替Integer
,我们就能线程安全地并发增加数值,而不需要同步访问变量。incrementAndGet()
方法是原子操作,所以我们可以在多个线程中安全调用它。
其他JDK1.5中提供的原子操作方法还有:
1 | public final int get() //获取当前的值 |
以下是JDK1.8开始支持的原子操作。updateAndGet()
接受lambda表达式,以便在整数上执行任意操作:
1 | private static AtomicInteger atomicInt = new AtomicInteger(0); |
accumulateAndGet()
方法接受另一种类型IntBinaryOperator
的lambda表达式。我们在下个例子中的操作和上一个例子的atomicInt.updateAndGet(n -> n + 2);
相同:
1 | private static AtomicInteger atomicInt = new AtomicInteger(0); |
其它实用的原子类有AtomicBoolean
、AtomicLong
和 AtomicReference
。
LongAdder
LongAdder
是AtomicLong
的替代,用于向某个数值连续添加值。long值的原子计算有些特殊,JVM会把64位的long值前后分成两个32位来分别进行操作,导致前32位是新值,而后32位仍是旧值。原来的AtomicLong
也能完成原子操作,但LongAdder
性能更高。
1 | private static LongAdder adder = new LongAdder(); |
LongAdder
提供了add()
和increment()
方法,就像原子数值类一样,同样是线程安全的。但是这个类在内部维护一系列变量来减少线程之间的争用,而不是求和计算单一结果。实际的结果可以通过调用sum()
或sumThenReset()
来获取。
当多线程的更新比读取更频繁时,这个类通常比原子数值类性能更好。这种情况在抓取统计数据时经常出现,例如,你希望统计Web服务器上请求的数量。LongAdder
缺点是较高的内存开销,因为它在内存中储存了一系列变量。
LongAccumulator
LongAccumulator
是LongAdder
的更通用的版本。LongAccumulator
以类型为LongBinaryOperator
lambda表达式构建,而不是仅仅执行加法操作,像这段代码展示的那样:
1 | private static void testAccumulate() throws InterruptedException { |
我们使用函数2 * x + y
创建了LongAccumulator
,初始值为1。每次调用accumulate(i)
的时候,当前结果和值i
都会作为参数传入lambda表达式。
LongAccumulator
就像LongAdder
那样,在内部维护一系列变量来减少线程之间的争用。
volatile关键字
对变量使用volatile关键字能让它具备可见性,也就是说当它的值发生改变时,所有线程都能及时在主存中看到它的新值。需要注意的是,他是不具备原子性的。比如下面的例子
1 | public class BadIncrementDemo { |
上面的代码按理讲应输出1000,而我的得到的时998。可见就算保证可见性不具备原子性,仍会发生错误。这是因为当值发生变化时,主存中的值确实会立即刷新,但时可能各个线程的工作内存中的值仍是旧值。
volatile的使用场景
那么到底该什么时候使用volatile关键字。实际上volatile的使用场景很少,如果要用必须具备一下两个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
也就是说,volatile变量应该独立于任何程序的状态,包括变量的当前状态。
下面是volatile的常用方式
状态标记量
1 | volatile boolean flag = false; |
双重锁
1 | public class Singleton { |