农场主的黑科技.

并发操作合集-5.原子变量和volatile

字数统计: 2.1k阅读时长: 12 min
2018/11/02 Share

🍤 并发操作合集系列 目录

🍕 并发操作合集系列 源代码

本章主要讲解原子变量和volatile关键字的使用方式和使用场景,包括Java8中新加入的LongAdder和LongAccumulator。想要了解原子变量和volatile关键字,就必须先了解Java内存模型中的原子性可见性

可见性和原子性

原子性

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

1
2
3
4
x = 10;         //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4

上面的语句中只有语句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()); //输出20000
}

通过使用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()); //输出4000
}

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 -> {
// 2是想要累计计算的值 , (n, m) -> n + m 表示 对2和目前的值做什么样的操作
//当然你也可以写成:
// atomicInt.accumulateAndGet(2, Integer::sum);
atomicInt.accumulateAndGet(2, (n, m) -> n + m);
});
};
new Thread(accumulate).start();
new Thread(accumulate).start();
TimeUnit.SECONDS.sleep(1);
System.out.println(atomicInt.get()); //输出4000
}

其它实用的原子类有AtomicBooleanAtomicLongAtomicReference

LongAdder

LongAdderAtomicLong的替代,用于向某个数值连续添加值。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);
// adder.increment();
});
};
new Thread(increment).start();
new Thread(increment).start();
TimeUnit.SECONDS.sleep(1);
System.out.println(adder.sumThenReset()); //输出20000,并归零
}

LongAdder提供了add()increment()方法,就像原子数值类一样,同样是线程安全的。但是这个类在内部维护一系列变量来减少线程之间的争用,而不是求和计算单一结果。实际的结果可以通过调用sum()sumThenReset()来获取。

当多线程的更新比读取更频繁时,这个类通常比原子数值类性能更好。这种情况在抓取统计数据时经常出现,例如,你希望统计Web服务器上请求的数量。LongAdder缺点是较高的内存开销,因为它在内存中储存了一系列变量。

LongAccumulator

LongAccumulatorLongAdder的更通用的版本。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()); //输出2086901
}

我们使用函数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());
});
};

//启动100个 执行10次cnt++的线程
IntStream.range(0, 100).forEach(i -> {
new Thread(increment).start();
});


TimeUnit.SECONDS.sleep(1);
System.out.println(cnt); //可能输出998
}
}

上面的代码按理讲应输出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关键字解析

CATALOG
  1. 1. 可见性和原子性
    1. 1.1. 原子性
    2. 1.2. 可见性
  2. 2. 原子变量
    1. 2.1. AtomicInteger
    2. 2.2. LongAdder
    3. 2.3. LongAccumulator
  3. 3. volatile关键字
    1. 3.1. volatile的使用场景
      1. 3.1.1. 状态标记量
      2. 3.1.2. 双重锁
  4. 4. Reference