可见性
造成可见性的最根本原因是CPU缓存
,并且还是多核CPU下的缓存
。换而言之如果是单CPU的缓存则不会存在可见性问题。
重排序
下面的代码有可能先执行ready = true; 然后再执行value=2,从而导致的结果就是t1线程输出的结果为1。
重排序也只会在多线程情况下才有可能发生问题的(请注意是多线程,不用关心CPU是否是多核)。
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
public class Demo30_3 {
private static boolean ready = false;
private static int value = 1;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!ready) {
}
System.out.println(value);
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
value = 2;
ready = true;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
重排序分类
- 编译优化导致的重排序
- CPU指令并行执行导致的重排序
- 硬件内存模型导致的重排序
原子性
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
public class Demo30_2 {
private static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; ++i) {
count++;
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; ++i) {
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
如何解决多线程中的可见性、原子性和有序性问题?
通过3个关键词分别是:volatile、synchronized、final,1个规则是happens-before规则。
volitile
volitile可以解决可见性问题,重排序问题和部分原子性问题(原子性很少会用到volitile)。
volitile可以让线程主动让值刷新到内存中和主动从内存中获取数据。
使用volitile来创建双重检验的单例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Singleton {
private static Singleton instance;
private int value;
private Singleton(int value) {
this.value = value;
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(11);
}
}
}
return instance;
}
// 省略其他方法,比如value的getter方法
}
instance = new Singleton(11); 不是原子性的,
- 为对象分配内存空间
- 初始化对象
- 将内存地址分配给instance
使用volitle来修饰instance,从容让将内存地址分配给instance
不能重排序到初始化对象和为分配对象空间之前。
假如不加volitile会造成将内存地址分配给instance
提前,但是对象还没有进行初始化,从而造成这个单例不可用。
一个常见的错误
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
private static boolean running = true;
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (running) {
count++;
}
System.out.println("count: " + count);
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
running = false;
}
});
t1.start();
Thread.sleep(1000); // 1s
t2.start();
t1.join();
t2.join();
}
比如上述代码,很多人用这个代码来证明多线程之间的可见性问题,但是其实是不正确的。如果是是可见性问题,那么当t2线程改变了running=true的时候,t1线程也不会一直永远进行while循环而不停止,但是实际情况确实t1线程一直在运行。
原因是:
在进行JIT编译时,JIT编译器会同步进行一定的编译优化。JIT编译器在编译线程t1中的while循环时,因为探测到running一直为true,所以,对其进行优化,省掉了每次判定running是否为true的逻辑.
认为优化后的代码为:
1
2
3
4
5
6
7
@Override
public void run() {
while (true) {
count++;
}
System.out.println("count: " + count);
}
可以给running之前加上volitile关键字,来让JIT优化不那么激进可以解决问题。
当然也可以给count来加上volitile,这样因为count++是一个写操作,会强制上面的指令不发生重排序(为了保证之前的读和写正确),然后让running去内存中获取。