Java内存模型中的synchronized和volatile是如何工作的?

4
在《Effective Java》一书中:
// Broken! - How long would you expect this program to run?
public class StopThread {

    private static boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (!stopRequested)
                    i++;
            }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

背景线程不会在一秒钟后停止。这是因为JVM中的优化——hoisting(提升)。HotSpot服务器VM会进行此优化。

您可以在以下主题中查看:
为什么HotSpot会使用hoisting进行优化?

此优化的过程如下:

if (!done)
    while (true)
        i++;

有两种方法可以解决这个问题。

1. 使用 volatile

private static volatile boolean stopRequested;

volatile的作用是:
- 禁止变量提升(hoisting)
- 保证任何读取该字段的线程都能看到最近写入的值

2. 使用synchronized

public class StopThread {

    private static boolean stopRequested;

    private static synchronized void requestStop() {
        stopRequested = true;
    }

    private static synchronized boolean stopRequested() {
        return stopRequested;
    }

    public static void main(String[] args)
                throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (!stopRequested())
                    i++;
            }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}

上述代码来自于《Effective Java》一书,在该书中使用了volatile修饰符来替代stopRequested变量。
private static boolean stopRequested() {
    return stopRequested;
}

如果这个方法省略了synchronized关键字,那么程序不会正常工作。
我认为这个改变会导致synchronized关键字的提升问题。
这样说对吗?

3个回答

5
为了清楚地理解为什么会发生这种情况,您需要知道更深层次的事情发生了什么(这基本上是所谓的发生前关系的解释,我希望用读者更易懂的语言来描述)。
通常变量存在于RAM内存中。当线程需要使用它们时,它们从RAM中取出并放入缓存中,以便在需要时尽快访问它们。
使用volatile强制一个线程直接从RAM内存中读取和写入变量。因此,当多个线程正在使用相同的volatile变量时,它们都看到的是RAM内存中最新版本,而不是可能存在于缓存中的旧副本。
当一个线程进入一个synchronized块时,它需要控制一个监视器变量。所有其他线程都等待第一个线程退出synchronized块。为确保所有线程可以看到相同的修改,synchronized块中使用的所有变量都直接从RAM内存中读取和写入,而不是从缓存副本中读取。
因此,如果您尝试在没有synchronized方法或没有volatile关键字的情况下读取变量stopRequested,则可能会读取存在于缓存中的旧副本。
为了解决这个问题,您需要确保:
  • 所有线程都使用volatile变量
  • 或者访问这些变量的所有线程都使用synchronized块。
使用该方法
private static boolean stopRequested() {
   return stopRequested;
}

如果没有使用 synchronized 关键字,且 stopRequested 不是 volatile,那么就可能从一个无效的缓存副本读取 stopRequested 的值。


谢谢,我想知道是否使用 volatilesynchronized 也会禁止提升 (hoisting)?如果不是,这个程序能运行正确吗? - Edward

0

这并不是问题。当同步方法退出时,它会自动与后续调用相同对象的同步方法建立happens-before关系。这保证了对象状态的更改对所有线程都是可见的。至于你上面的问题,volatilesynchronized都保证了可见性,这使得它在1秒后停止。


是的,你说得对。但我想问的是,如果在方法stopRequested中省略synchronized关键字,而在方法requestStop中仍然存在该关键字,那么执行requestStop方法可以改变stopRequested的值,并被其他线程看到。如果没有发生“提升”,我认为这个程序是正确运行的。 - Edward
@Edward,stopRequestedsynchronized保证了每个进入该方法的线程都可以看到最新的变量,synchronized是不可省略的。你是否认为一旦synchronized,则被阻塞的变量的更改始终是可见的? - passion

0

声明一个Java的volatile变量意味着:

这个变量的值永远不会被线程本地缓存:所有的读写操作都将直接进入“主内存”

声明同步意味着:
因此,volatile仅在线程内存和“主”内存之间同步一个变量的值,而synchronized在方法中同步所有变量在线程内存和“主”内存之间的值,并锁定和释放监视器以控制多个线程之间的所有权。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接