《有效编程》一书中的代码:
public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
int i = 0;
while (!stopRequested){
i++;
}
System.out.println(i);
});
t.start();
Thread.sleep(10);
stopRequested = true;
}
}
此代码导致线程 t 内的无限循环。但是如果在给定的循环中,添加 System.out.printl();
public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
int i = 0;
while (!StopThread.stopRequested){
i++;
System.out.println();
}
System.out.println(i);
});
t.start();
Thread.sleep(10);
stopRequested = true;
}
}
在这种情况下,循环被中断。这个怎么运作?
在原始代码中,
stopRequested没有volatile任何块synchronized(或其他使更改在线程之间可见的方法)。因此,正如预期的那样,这样的循环并没有结束。添加时,
System.out.println()情况会发生变化。内部System.out.println有一个同步点(即从volatile变量或synchronized块读取),因此stopRequested在另一个线程中所做的更改对该线程可见。我将补充一点关于为什么原始循环没有结束的原因。内存访问操作非常昂贵,可以认为它比使用 L1 缓存慢 100 倍。如果对变量的每次访问都读取或写入内存,那么在绝大多数情况下,这将不必要地显着减慢程序速度。
因此,java 规范允许实现省略一些内存访问并使用线程本地缓存。Java 内存模型是规范的一部分,它描述了读取和写入内存所需的操作的正式规则(为了更好地理解,我在这里以简化的方式编写)。程序员知道一个变量何时可以被多个线程使用,并且必须明确地使用机制来确保更改对其他线程可见。
特别是,
volatile同步块之外的普通(非)变量不需要被环境同步。并且任何足够优化的实现都会这样做。因此,从规范的角度来看,在一个线程中所做的更改在另一个线程中不可见这一事实是预期的,但是,我同意,对于多线程的新手来说,这远非显而易见。不。Java 规范中没有任何内容要求发生这种情况。
t可以永远看到变量的初始值,并且不会违反规范。我不确切知道它是如何工作
println的,但是在其中执行synchronized块或函数或从volatile变量读取就足够了,这将导致在主内存中所做的所有更改都将对我们的线程可见。