Java 并发编程之 Volatile 和 Automic

Catalogue
  1. 1 volatile 的特性
    1. 1.1 volatile 的典型使用场景
    2. 1.2 从 JMM 理解 synchronized、volatile 等关键字
  2. 2 AtomicInteger介绍

1 volatile 的特性

这篇文章对于 volatile 介绍得很直观,一定要通读。文章链接>>

以下是对 volatile 作的补充。

1.1 volatile 的典型使用场景

1、用来标示状态量。 状态量标示就是通过一个 boolean 类型变量来判断逻辑是否需要执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private static volatile boolean flag = false;

private static void refershFlag() throws InterruptedException {

Thread threadA = new Thread(new Runnable() {

@Override
public void run() {
while (!flag) {
//do something
}
}
});

Thread threadB = new Thread(new Runnable() {

@Override
public void run() {

flag = true;
}
});

DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");

System.out.println("threadA start" + dateFormat.format(new java.util.Date()));
threadA.start();

Thread.sleep(100);

threadB.start();

threadA.join();
System.out.println("threadA end" + dateFormat.format(new java.util.Date()));
}

//threadA start2017/02/21 10:48:41
//threadA end2017/02/21 10:48:41

2、double-check 问题应该是 volatile 使用最多的场景了。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class DoubleCheck {

private volatile static DoubleCheck instance = null;

private DoubleCheck() {

}

public static DoubleCheck getInstance() {

if (null == instance) { //步骤一
synchronized (DoubleCheck.class) {
if (null == instance) { //步骤二
instance = new DoubleCheck(); //步骤三
}
}
}
return instance;
}

public static void main(String[] args) throws InterruptedException {

DoubleCheck doubleCheck = DoubleCheck.getInstance();
}
}

代码中步骤三并不是原子性的,可以分为三步:
3.1 为 DoubleCheck 在堆中申请一块内存空间(alloc memory address)
3.2 初始化对象 DoubleCheck(init DoubleCheck)
3.3 将申请的内存空间引用地址赋值给 instance 引用变量(instance > memory address)
在 JVM 看来3.2和3.3并不存在依赖关系,是有可能会重排序的,如果将3.2和3.3重排序:

线程2在步骤一时判断 instance 不为空的情况下,实际上对象并没有初始化,3.2并没有执行。导致接下来使用对象发生错误。此时使用 volatile 修饰 instance 变量就可以防止3.2和3.3重排序,这样就保证了多线程访问时代码的正确性。
3、下面的场景不是经常出现,但是可以作为对 volatile 完整理解的引子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class VolatileClass {
public static void main(String[] args) throws InterruptedException {
AAndB aAndB = new AAndB();
for (int i = 0; i < 10000000; i++) {
AThread aThread = new AThread(aAndB);
BThread bThread = new BThread(aAndB);
aThread.start();
bThread.start();
aThread.join();
bThread.join();
if (aAndB.x == 0 && aAndB.y == 0) {
System.out.println("resort");
}
aAndB.x = aAndB.y = aAndB.a = aAndB.b = 0;
}

System.out.println("end");
}

public static class AAndB {

volatile int x = 0;
volatile int y = 0;
volatile int a = 0;
volatile int b = 0;

public void awrite() {
a = 1;
x = b;
}

public void bwrite() {
b = 1;
y = a;
}
}

public static class AThread extends Thread {
private AAndB aAndB;

public AThread(AAndB aAndB) {
this.aAndB = aAndB;
}

@Override
public void run() {
super.run();
this.aAndB.awrite();
}
}

public static class BThread extends Thread {

private AAndB aAndB;

public BThread(AAndB aAndB) {

this.aAndB = aAndB;
}

@Override
public void run() {
super.run();
this.aAndB.bwrite();
}

}

}
// end

1、x,y 使用 volatile 修饰是为了避免 JVM 的重排序问题,这个在本文开头的链接中有提。
2、a,b 使用 volatile 修饰是为了保证数据的可见性。
3、所以,只有 x,y,a,b 全部都用 volatile 修饰才能保证 x,y 不同时为 0 。

参考链接>>
(PS:这里只是习惯性地注明知识的来源。对于文链中的其余部分笔者还存有疑惑,请谨慎参考!而且,对于场景三笔者和文链中的观点是有些许不同的。)

1.2 从 JMM 理解 synchronized、volatile 等关键字

参考链接>>

2 AtomicInteger介绍

原子操作在多线程场景是很有必要的,它可以避免数据的二义性产生。下面的程序中我们新建了两个线程,每个线程对同一个数增加了4次,每次加1,最后的结果正确应该是8。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class JavaAtomic {

public static void main(String[] args) throws InterruptedException {

ProcessingThread pt = new ProcessingThread();
Thread t1 = new Thread(pt, "t1");
t1.start();
Thread t2 = new Thread(pt, "t2");
t2.start();
t1.join();
t2.join();
System.out.println("Processing count=" + pt.getCount());
}

}

class ProcessingThread implements Runnable {
private int count;

@Override
public void run() {
for (int i = 1; i < 5; i++) {
processSomething(i);
count++;
}
}

public int getCount() {
return this.count;
}

private void processSomething(int i) {
// processing some job
try {
Thread.sleep(i * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

运行上面的程序后,你会发现最后的运行结果每次都不一样,可能是5,6,7或者8。原因就是 count++ 不具备原子性,当一个线程读取了 count 的值并进行完加1操作时,另外一个线程还是引用的原来旧的 count 值,这样就导致了最后加1后的结果不符合预期的现象。
为了解决这个问题,我们除了可以使用 Synchronized、Lock,还可以使用 Java 5 java.util.concurrent.atomic 提供的 AutomicInteger 实现原子操作。并且在程序逻辑允许的情况下,我们应该优先使用 atomic,它在执行速度、可读性、实用性方面均优于前两者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class JavaAtomic {

public static void main(String[] args) throws InterruptedException {

ProcessingThread pt = new ProcessingThread();
Thread t1 = new Thread(pt, "t1");
t1.start();
Thread t2 = new Thread(pt, "t2");
t2.start();
t1.join();
t2.join();
System.out.println("Processing count=" + pt.getCount());
}
}

class ProcessingThread implements Runnable {
private AtomicInteger count = new AtomicInteger();

@Override
public void run() {
for (int i = 1; i < 5; i++) {
processSomething(i);
count.incrementAndGet();
}
}

public int getCount() {
return this.count.get();
}

private void processSomething(int i) {
// processing some job
try {
Thread.sleep(i * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

还有一个问题是,为什么 AutomicInteger 可以实现原子操作呢?这和它内部实现的 compareAndSwap(CAS) 有关系:

1
2
3
4
5
6
7
8
9
10
11
12
//java 8
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}

通过与预期的值做比较,如果当前值和预期值相同,则说明没有被其他线程更改过,就可以放心的设置新增。否则重复检查直到赋值成功。

参考链接>>