前言
前一阵子学习多线程时,学到volatile是保证了线程之间的可见性,volatile对应的内存语义为:
1、写一个volatile变量时,将工作内存共享变量刷新到主内存
2、读一个volatile变量时,将工作内存置为无效,从主内存中读取共享变量
对于非volatile变量,如果没有加锁的操作,就不能保证多线程之间是可见的,但是在练习的时候就碰到了如下的一个例子:
例子
话不多说,直接上代码
public class test {
static boolean flag = true;
static int x = 0;
public static void main(String[] args) throws InterruptedException {
System.out.println("初始值:" + x);
Thread t1 = new Thread(() -> {
while(flag){
x=1;
}
System.out.println("更改值:" + x);
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("end");
}
}
这段代码首先是设置了一个共享变量flag,默认为true,线程t1在flag=true
的时候执行循环,线程t2会的功能是设置flag=false,本意是使得线程t1可以退出循环,而t2执行的时候sleep一段时间也是保证t1先行与t2运行。运行的结果如下所示:
可以看到,整个程序一直保持运行无法正常结束,由此可以推知t2线程对共享变量的修改t1线程是看不到的,这也符合Java内存模型工作内存和主内存的预期,但是如果我们修改t1循环内的代码后,如下:
public class test {
static boolean flag = true;
static int x = 0;
public static void main(String[] args) throws InterruptedException {
System.out.println("初始值:" + x);
Thread t1 = new Thread(() -> {
while(flag){
x=1;
Object[] onums1 = new Object[10000];
Object[] onums2 = new Object[10000];
Object[] onums3 = new Object[10000];
Object[] onums4 = new Object[10000];
}
System.out.println("更改值:" + x);
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
});
t1.start();
t2.start();
t2.join();
t1.join();
System.out.println("end");
}
}
则再次运行后的结果为:
神奇的是,t1线程并没有一直在循环中,由此可以推知这种情况下t1线程一定是重新从主内存中再次获取到了共享变量,那为什么就增加了创建几个大数组就可以让线程重新去主内存刷新共享变量呢?这就涉及到工作内存的刷新时机
另一个例子
首先我们来看网上有一个更为常见的例子,就是上述循环的代码块里面写一个print语句,类似这样:
while(flag){
x=1;
System.out.println("确认更改值:" + x);
}
其余代码不变,最终线程t1也能够感知到flag状态的更改而结束循环,这种情况是因为System.out.println
中是存在synchronized
锁的,源代码如下:
public void println(String x) {
if (getClass() == PrintStream.class) {
writeln(String.valueOf(x));
} else {
synchronized (this) {
print(x);
newLine();
}
}
}
我们都知道,锁的内存语义为:
1、线程获取锁时,JMM会把该线程对应的本地内存置为无效
2、线程释放锁时,JMM会把该线程对应的本地内存的共享变量刷新到主内存
因此这种情况线程t1能够获取到共享变量flag的新值也不足为奇。
工作内存刷新时机
多方资料搜集查验后,广泛流传的几个刷新时机总结如下:
1、线程获取锁时
2、线程切换到其他线程,再切换回来之后
3、该线程的CPU有空闲时(或者说涉及CPU时间片轮转时),比如当前线程sleep了
再回到第一个例子,其实最开始测试的时候new产生的数组设置为了1000,此时仍然会执行死循环,因此导致无法退出死循环的直接原因是创建的数组不够大。而对象创建的过程主要步骤为:
类加载检查->分配内存->初始化零值->设置对象头->执行init方法
因此,根据上述已有知识的推测,如果创建的对象数组过大,会导致这个流程耗时时间长,当前线程所持有的CPU在new对象数组的时候,相当于sleep了一个小时间,这个时间内CPU就会去主内存获取贡献变量的值刷新到工作内存
总结
工作内存中的非volatile修饰的共享变量大致会在上述三种情况下去主内存重新刷新变量值,但是对于需要保证可见性的共享变量,尽可能还是用锁或者volatile进行修饰,确保每次得到的共享变量值都是最新的。
补充
后面发现一个有意思的现象,就是我们不是直接run代码,而是debug,在while循环内的x=1语句上打断点,如下:
debug执行后,等待两秒再放行,发现此时可以得到flag的值为t2线程修改后的false
这进一步验证了当CPU并不是被完全占用的时候,会从主内存去刷新共享变量的值