jmmIOVisibility
IO操作引起的内存可见性
问题
@Data
public class ChangeThread implements Runnable {
/**
* volatile
**/
boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("subThread change flag to:" + flag);
flag = true;
System.out.println("subThread change flag to:" + flag);
}
}
public static void main(String[] args) {
ChangeThread changeThread = new ChangeThread();
new Thread(changeThread).start();
while (true) {
if (changeThread.isFlag()) {
System.out.println("detected flag changed");
break;
}else {
// System.out.println("false flag ...");
}
}
System.out.println("main thread end");
}
代码块二中:main函数的else中System.out.println是否执行对结果的影响?
结论:
执行System.out.println,主线程会按照预期退出;
不执行System.out.println的话,主线程会一直执行。
从结果上看,System.out.println 导致flag的值,从主内存(强制?)刷新到main线程的工作内存;打印语句被注释掉时,mainThread
可能会不断地在自己的缓存中读取 flag
的值,而不是从主内存中读取。如果 flag
的值没有被修改,这种读取可能会一直读取到旧值,从而导致 mainThread
进入一个无限循环。
分析:
一、System.out.println
属于输入/输出(I/O)操作中的输出操作。它将数据输出到控制台,这涉及与操作系统的交互来处理输出设备(通常是显示器)。打印操作引发 I/O 操作,I/O 操作通常是缓慢的,会导致线程的状态发生变化。这里尝试其他io操作(log等)是同样的效果。
I/O 操作的副作用:I/O 操作(如打印)通常会涉及底层的系统调用,可能会导致线程调度器重新调度线程。这种调度可能会导致 mainThread
从主内存中重新加载变量,从而看到最新的值。
优化行为:JVM 和 CPU 的优化行为可能在某些情况下使得打印操作刷新了缓存行或工作内存,但这种行为并不可靠或可预见。
二、System.out
是一个 PrintStream
对象,而 PrintStream
的写操作通常会调用内部的同步方法。每次调用 println
时,都会获得 PrintStream
对象的锁,并在释放锁之前执行写操作。锁机制会确保内存的可见性:当一个线程释放一个锁时,Java 内存模型会确保这个线程对共享变量的修改对随后获得这个锁的线程可见。
但是,获取和释放 PrintStream
的锁与 flag
的内存可见性之间,应该并没有直接关系。
解决:
1.volatile 修饰flag变量,保证内存可见性
2.unsafe.loadFence() ,保证在这个屏障之前的所有读操作都已经完成,并且将缓存数据设为无效,重新从主存中进行加载。
延申:
一、jmm 什么清空下会触发,重新加载主存中数据
在 Java 内存模型(Java Memory Model, JMM)中,重新加载主存中数据通常发生在以下几种情况下:
- 线程启动:
- 当一个线程启动时,它会清空其工作内存(线程的本地缓存)中的数据,并从主存中重新加载所有共享变量的最新值。
volatile
变量:- 对
volatile
变量的读写操作会导致线程直接从主存中读取数据或将数据写回主存。具体来说,当一个线程读取volatile
变量时,它会强制从主存中读取最新的值;当一个线程写入volatile
变量时,它会将新值立即刷新到主存。
- 对
- 锁和解锁操作:
- 线程在执行
synchronized
块或方法时,当它获取锁时,会清空工作内存,并从主存中重新加载被监视对象的所有共享变量。当释放锁时,会将工作内存中对这些变量的修改刷新到主存。 - 这也适用于
Lock
接口及其实现,例如ReentrantLock
。
- 线程在执行
final
变量:- 对于
final
变量,在对象构造器内的赋值操作会确保对象构造完毕且构造器完成后,final
变量的值对所有其他线程可见。
- 对于
- 线程终止:
- 当一个线程终止时,所有对共享变量的修改都会被刷新到主存,确保其他线程可以看到这些修改。
二:内存间交互操作(深入理解jvm)
·lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
·unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
·read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
·load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
·use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
·assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
·store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
·write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
如果要把一个变量从主内存拷贝到工作内存,那就要按顺序执行read和load操作,如果要把变量从工作内存同步回主内存,就要按顺序执行store和write操作。注意,Java内存模型只要求上述两个操作必须按(相对)顺序执行,但不要求是连续执行。也就是说read与load之间、store与write之间是可插入其他指令的。