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