Skip to content

Commit 7f8ab3d

Browse files
committed
✨ Introducing new features. volatile over
1 parent 4042e12 commit 7f8ab3d

File tree

3 files changed

+243
-1
lines changed

3 files changed

+243
-1
lines changed

MD/concurrent/volatile.md

+172-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,178 @@
2525
2626
### 内存可见性的应用
2727

28+
当我们需要在两个线程间依据主内存通信时,通信的那个变量就必须的用 volatile 来修饰:
29+
30+
```java
31+
public class Volatile implements Runnable{
32+
33+
private static volatile boolean flag = true ;
34+
35+
@Override
36+
public void run() {
37+
while (flag){
38+
System.out.println(Thread.currentThread().getName() + "正在运行。。。");
39+
}
40+
System.out.println(Thread.currentThread().getName() +"执行完毕");
41+
}
42+
43+
public static void main(String[] args) throws InterruptedException {
44+
Volatile aVolatile = new Volatile();
45+
new Thread(aVolatile,"thread A").start();
46+
47+
48+
System.out.println("main 线程正在运行") ;
49+
50+
TimeUnit.MILLISECONDS.sleep(100) ;
51+
52+
aVolatile.stopThread();
53+
54+
}
55+
56+
private void stopThread(){
57+
flag = false ;
58+
}
59+
}
60+
```
61+
62+
主线程在修改了标志位使得线程 A 立即停止,如果没有用 `volatile` 修饰,就有可能出现延迟。
63+
64+
但这里有个误区,这样的使用方式容易给人的感觉是:
65+
66+
> `volatile` 修饰的变量进行并发操作是线程安全的。
67+
68+
这里要重点强调,`volatile`**不能**保证线程安全性!
69+
70+
如下程序:
71+
72+
```java
73+
public class VolatileInc implements Runnable{
74+
75+
private static volatile int count = 0 ; //使用 volatile 修饰基本数据内存不能保证原子性
76+
77+
//private static AtomicInteger count = new AtomicInteger() ;
78+
79+
@Override
80+
public void run() {
81+
for (int i=0;i<10000 ;i++){
82+
count ++ ;
83+
//count.incrementAndGet() ;
84+
}
85+
}
86+
87+
public static void main(String[] args) throws InterruptedException {
88+
VolatileInc volatileInc = new VolatileInc() ;
89+
Thread t1 = new Thread(volatileInc,"t1") ;
90+
Thread t2 = new Thread(volatileInc,"t2") ;
91+
t1.start();
92+
//t1.join();
93+
94+
t2.start();
95+
//t2.join();
96+
for (int i=0;i<10000 ;i++){
97+
count ++ ;
98+
//count.incrementAndGet();
99+
}
100+
101+
102+
System.out.println("最终Count="+count);
103+
}
104+
}
105+
```
106+
107+
当我们三个线程(t1,t2,main)同时对一个 `int` 进行累加时会发现最终的值都会小于 30000。
108+
109+
> 这是因为虽然 `volatile` 保证了内存可见性,保证了每个线程拿到的值都是最新值,但 `count ++` 这个操作并不是原子的,这里面涉及到获取值、自增、赋值的操作并不能同时完成。
110+
>
111+
112+
- 所以想到达到线程安全可以使这三个线程串行执行(那其实就是单线程,没有发挥多线程的优势)。
113+
114+
- 也可以使用 `synchronize` 或者是锁的方式来保证原子性。
115+
116+
- 还可以用 `Atomic` 包中 `AtomicInteger` 来替换 `int`,它利用了 `CAS` 算法来保证了原子性。
117+
28118

29119
## 指令重排
30120

31-
### 指令重排的的应用
121+
内存可见性只是 volatile 的其中一个作用,它还可以防止 JVM 进行指令重排优化。
122+
123+
举一个伪代码:
124+
125+
```java
126+
int a=10 ;//1
127+
int b=20 ;//2
128+
int c= a+b ;//3
129+
```
130+
131+
一段特别简单的代码,理想情况下它的执行顺序是:`1>2>3`。但有可能经过 JVM 优化之后的执行顺序变为了 `2>1>3`
132+
133+
可以发现不管 JVM 怎么优化,前提都是保证最终结果不变的情况下进行的。
134+
135+
可能这里还看不出有什么问题,那看下一段伪代码:
136+
137+
```java
138+
private static Map<String,String> value ;
139+
private static volatile boolean flag = fasle ;
140+
141+
//以下方法发生在线程 A 中 初始化 Map
142+
public void initMap(){
143+
//耗时操作
144+
value = getMapValue() ;//1
145+
flag = true ;//2
146+
}
147+
148+
149+
//发生在线程 B中 等到 Map 初始化成功进行其他操作
150+
public void doSomeThing(){
151+
while(!flag){
152+
sleep() ;
153+
}
154+
//dosomething
155+
doSomeThing(value);
156+
}
157+
158+
```
159+
160+
这里就能看出问题了,当 `flag` 没有被 `volatile` 修饰时,`JVM` 对 1 和 2 进行重排,导致 value 都还没有被初始化就有可能被线程 B 使用了。
161+
162+
所以加上 `volatile` 之后可以防止这样的重排优化,保证业务的正确性。
163+
### 指令重排的的应用
164+
165+
一个经典的使用场景就是双重懒加载的单例模式了:
166+
167+
```java
168+
public class Singleton {
169+
170+
private static volatile Singleton singleton;
171+
172+
private Singleton() {
173+
}
174+
175+
public static Singleton getInstance() {
176+
if (singleton == null) {
177+
synchronized (Singleton.class) {
178+
if (singleton == null) {
179+
//防止指令重排
180+
singleton = new Singleton();
181+
}
182+
}
183+
}
184+
return singleton;
185+
}
186+
}
187+
```
188+
189+
这里的 `volatile` 关键字主要是为了防止指令重排。
190+
191+
如果不用 ,`singleton = new Singleton();`,这段代码其实是分为三步:
192+
- 分配内存空间。(1)
193+
- 初始化对象。(2)
194+
-`singleton` 对象指向分配的内存地址。(3)
195+
196+
加上 `volatile` 是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。
197+
198+
## 总结
199+
200+
`volatile``Java` 并发中用的很多,比如像 `Atomic` 包中的 `value`、以及 `AbstractQueuedLongSynchronizer` 中的 `state` 都是被定义为 `volatile` 来用于保证内存可见性。
201+
202+
将这块理解透彻对我们编写并发程序时可以提供很大帮助。
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.crossoverjie.concurrent;
2+
3+
/**
4+
* Function:
5+
*
6+
* @author crossoverJie
7+
* Date: 09/03/2018 01:14
8+
* @since JDK 1.8
9+
*/
10+
public class Singleton {
11+
12+
private static volatile Singleton singleton;
13+
14+
private Singleton() {
15+
}
16+
17+
public static Singleton getInstance() {
18+
if (singleton == null) {
19+
synchronized (Singleton.class) {
20+
if (singleton == null) {
21+
//防止指令重排
22+
singleton = new Singleton();
23+
}
24+
}
25+
}
26+
return singleton;
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.crossoverjie.concurrent;
2+
3+
import java.util.concurrent.atomic.AtomicInteger;
4+
5+
/**
6+
* Function:
7+
*
8+
* @author crossoverJie
9+
* Date: 09/03/2018 00:34
10+
* @since JDK 1.8
11+
*/
12+
public class VolatileInc implements Runnable{
13+
14+
private static volatile int count = 0 ; //使用 volatile 修饰基本数据内存不能保证原子性
15+
16+
//private static AtomicInteger count = new AtomicInteger() ;
17+
18+
@Override
19+
public void run() {
20+
for (int i=0;i<10000 ;i++){
21+
count ++ ;
22+
//count.incrementAndGet() ;
23+
}
24+
}
25+
26+
public static void main(String[] args) throws InterruptedException {
27+
VolatileInc volatileInc = new VolatileInc() ;
28+
Thread t1 = new Thread(volatileInc,"t1") ;
29+
Thread t2 = new Thread(volatileInc,"t2") ;
30+
t1.start();
31+
//t1.join();
32+
33+
t2.start();
34+
//t2.join();
35+
for (int i=0;i<10000 ;i++){
36+
count ++ ;
37+
//count.incrementAndGet();
38+
}
39+
40+
41+
System.out.println("最终Count="+count);
42+
}
43+
}

0 commit comments

Comments
 (0)