关于一个单例的困惑

一个说法不去验证就欣然接受,然后再把它传播给别人,荒谬又可悲!

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
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class DoubleIfSingleton {
private static DoubleIfSingleton ins = null; //private static volatile DoubleIfSingleton ins = null;

public static void main(String[] args) throws InterruptedException {
int max = 64;
CountDownLatch d = new CountDownLatch(1);
class R implements Runnable {
private CountDownLatch d;

public R(CountDownLatch d) {
this.d = d;
}

public void run() {
try {
d.await();
DoubleIfSingleton.getIns();
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

Thread[] ts = new Thread[max];
for (int i = 0; i < max; i++) {
ts[i] = new Thread(new R(d));
ts[i].start();
}

d.countDown();
for (int i = 0; i < max; i++)
ts[i].join();
}

public static DoubleIfSingleton getIns() {
if (null == ins) {
synchronized (DoubleIfSingleton.class) {
if (null == ins) {
ins = new DoubleIfSingleton();
}
}
}
return ins;
}

private DoubleIfSingleton() {
System.out.println(this.hashCode());
}
}

通过javap命令查看带volatile和不带volatile的class,运行命令竟然是一样的!
我怀疑是jdk在编译时做了优化。如果换成int呢?

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
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class IncreaseT {
public static int t = 0; //public volatile static int t = 0;

public static synchronized void add() {
for (int j = 0; j < 10000; j++)
t++;
}

public static void main(String[] args) throws InterruptedException {
int max = 128;
CountDownLatch d = new CountDownLatch(1);
class R implements Runnable {
private CountDownLatch d;

public R(CountDownLatch d) {
this.d = d;
}

public void run() {
try {
d.await();
IncreaseT.add();
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

Thread[] ts = new Thread[max];
for (int i = 0; i < max; i++) {
ts[i] = new Thread(new R(d));
ts[i].start();
}

d.countDown();
for (int i = 0; i < max; i++)
ts[i].join();
System.out.println(IncreaseT.t);
}
}

javap对比两者的命令不同,volatile没有被编译优化忽略。

所有博客和书都说这种单例模式必须加volatile,然后把java内存模型和happens-before原则拿来扯一扯。唉,可能之前的synchronized和jdk版本的问题,使得这种说法成立,但是现在来看加与不加并没有任何区别,如果我错了请邮件(zxfspace@163.com)通知我,感激不尽。

2017.12.29,今天真是个值得高兴的日子,这个困惑终于解决了。读了单例模式这篇文章,明白了原来自己一直忽略了对象产生的步骤和volatile的禁止指令重排作用,字节码好像是去掉了,但是终究不能完全相信字节码,因为它跟cpu真正执行的情况还是有区别的。
一个class的实例被new出来,过程包括三个部分:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
volatile就是禁止这3个指令重排,那么多线程情况下就不会出现B线程得到一个没有初始化的实例。