保证线程可见性最简单的方法是使用volatile修饰变量,对象(不保证被修饰对象的内部对象的可见性),或者CAS自旋,synchronized来完成
CAS自旋参考java.util.concurrent.atomic下的 AtomicBoolean 等实现compareAndSwap,但是是个硬件级别的本地方法.
废话不多说,上代码,看注释
package thread.base.visibility;
import java.util.concurrent.TimeUnit;
/**
*
* @author ZhenWeiLai
*
*/
public class TestVisibility {
String str = null;//将会被另一个线程修改
String temporary = null;//保存str被修改前的值
String template = "ABC";//String模板,循环连接直到超出CPU缓存
//构造时连接字符串
public TestVisibility() {
StringBuilder sb = new StringBuilder();
/**
* 在我的CPU(4790K)上,这里设置为 100000 就能达到终止循环的效果,取决于CPU的缓存大小
* 把循环次数减少,能达到被CPU缓存,到达死循环的效果
*/
for (int i = 0; i < 100000; i++) {
sb.append(template);
}
temporary = sb.toString();
str = sb.toString();
}
public void increment() {
/**
* 判断str的值是否等于temporary的值,如果str存在cpu缓存,那么将会是死循环
* 如果不存在cpu缓存,那么将会去主内存查找,循环将结束
*/
while (str.equals(temporary)) {
//注意这里不能System.out 因为这样会强制刷新cpu缓存中str的值,原因不明
//有的说法是jvm在一个变量读写频率很高的情况下,才不会把数据及时写进内存,但是如果在这里调用System.out.println的话,那么读取str的频率将会变低,所以就写入了内存
/**
* Thread.yield() 可以让步CPU,有可能从主内存中刷新数据
* 我的理解是,把CPU让出来让其他线程执行,那么缓存很可能被其他数据给使用了
* 导致不得不去主内存中重新读取
*/
// Thread.yield();
}
}
public static void main(String[] args) {
TestVisibility tv = new TestVisibility();
//一个线程开启执行循环方法
new Thread(() -> {
try {
tv.increment();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
try {
//主线程等待两秒,让循环飞一会
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 另一个线程修改str的值
*/
new Thread(() -> {
tv.str = "aa";
}).start();
}
}