前言
最近在看java并发实战,受益匪浅,但是有觉得有种囫囵吞枣的感觉,工作以后的学习,没有学校里面的系统,第一,没有老师教,第二,没有进度规划,第三,眼高手低缺失实战。
打算写这个微博系列,目的一,监督自己学习,目的二,将经验和教训与大家共享。
可见性
可见性是jvm的内存机制引入的问题,时间和空间用于都是一个矛盾的话题,为了提升效率,每个线程的内存和主内存直接存在一个同步过程。
具体可以参考java内存分布
我们看下书中的例子
public class NoVisibility {
private static boolean ready;
private static int number;
public static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println("number:" + number);
}
}
public static void main(String[] args) {
ReaderThread readerThread = new ReaderThread();
readerThread.start();
number = 42;
ready = true;
}
}
按书中描述,可能会死循环,可能会打印出来number的值为0。
但是,一直只会打印number:42,所有尝试只有这一种结果。
但是很遗憾,我这边试了很久,包括不断创建线程测试,还是没有复现出来。
是书中讲错了,还是我们试验的方式不对。
思路一 两个线程速度问题
主线程运行完,子线程才启动,导致子线程每次读取的值,已经是变化的。修改代码如下:
public class NoVisibilitySleep {
private static boolean ready;
private static int number;
public static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println("number:" + number);
}
}
public static void main(String[] args) {
ReaderThread readerThread = new ReaderThread();
readerThread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
number = 42;
ready = true;
}
}
结果还是一样,number:42
思路二 Thread.yield的作用
去掉Thread.yield
public class NoVisibilitySleepWithoutYield {
private static boolean ready;
private static int number;
public static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
// Thread.yield();
}
System.out.println("number:" + number);
}
}
public static void main(String[] args) {
ReaderThread readerThread = new ReaderThread();
readerThread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
number = 42;
ready = true;
}
}
这个时候神奇的事情发生了,这个程序永远是个死循环。(两个条件同时满足的时候就会出现死循环)
但是,如果你用debug模式在while地方打断点,或者是在循环里面加入System.out.println之类的话,也通用不会出现死循环。
理论上打印不应该改变程序的,但是这样奇怪的现象出现了。
有三篇文章关于这个问题:
- R大在知乎上的 R大解释
- csdn上的文章 趣谈可见性
- 可见性理解 其中文章1和3的大部分观点是一致的,对应死循环的原因是,如果while里面是个空,或者是number++之类的,编译器会进行优化代码变成了
int localReady = ready
while (!localready) {
}
localReady只会赋值一次,所以就会导致死循环,加入了System.out.println编译器没有优化,所以就会输出结果。
可见性和代码优化是同一件事情不同表达方式,正是由于jvm自己的内存结构,会对代码进行相应的优化。可见性,主内存和线程内存的拷贝是底层数据的表现形式,代码优化是具体实现手段。
所以,书中说了可能永远不可见,指的就是这种情况。
但是为什么Thread.yield和Thread.sleep在循环中出现,程序能够直接运行,没有一个准确的答案,官方说是不会影响的,但是现实中确实如此。
上面的例子证明了可见性了么?其实没有完全证明,要想按书上的例子证明其实很困难,依赖于环境,jvm版本,参数配置等等,但是理论上是完全有可能出现的。
笔者的可以通过另外一个例子发现可见性java多线程学习3-重排序
总结
目前无法复现书中例子,原因我目前没有探明,需要查询更多资料得到原因。
得出结论是,对于这种标志位形式的变量,一般只有set和get等原子操作的,引入引入关键字volitate,可以让代码不优化,时刻读取主内存的元素。
但是volitate只能保证可见性,对于原子性无法保证。下次我们在看看原子性