有效Java 第66条:为什么要同步读写方法?

Effective Java Item 66: Why to synchronize both read and write methods?

在 Effective Java -> Item 66 中,Joshua 强调需要同步读写操作以避免活性失败。

在此特定示例中,我认为写同步方法是多余的。即使删除同步写入方法后,程序运行和终止也没有任何问题。 需要同步才能看到对象的一致状态,这是通过同步读取方法实现的。

请让我知道您对此的看法。

import java.util.concurrent.TimeUnit;

public class StopThread {

private static boolean stopRequested;

public static void main(String[] args) throws InterruptedException {
    new Thread(new Runnable() {

        @Override
        public void run() {
            int i = 0;
            while (!isStopRequested())
                i++;
        }
    }).start();
    ;
    TimeUnit.SECONDS.sleep(1);
    setStopRequested(true);
}

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

private static void setStopRequested(boolean stopRequested) {
    StopThread.stopRequested = stopRequested;
}
}

我稍微修改了你的代码,现在它根本没有完成。当你不使用原子变量时,你永远不会知道接下来会发生什么。 JIT 可以优化您 read/write 操作。

import java.util.concurrent.TimeUnit;

public class StopThread {

    private static boolean stopRequested = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {

            @Override
            public void run() {
                int i = 0;
                while (!stopRequested)
                    i++;
            }
        }).start();
        TimeUnit.SECONDS.sleep(10);
        stopRequested = true;
        System.out.println("Set to true");
        Thread.sleep(40 * 1000L);
    }
}

在我的机器上,这段代码永远不会完成。由于修改后的代码几乎完成相同的工作,因此很容易看出您依赖于将来可能会改变的 JIT 行为。也许在下一个 java 版本中,您的代码也将无法完成。

您提到的示例可能最适合演示在没有同步(或易失性)的情况下如何无法保证何时将线程本地内存中的值刷新到主内存,但这个例子肯定不是最适合演示 "read-write concurrency issues".

我想你可能误解了示例的目的,目的是为了展示在没有同步的情况下线程通信的效果。阅读以下同一项目 #66 的摘录:

The actions of the synchronized methods in StopThread would be atomic even without synchronization. In other words, the synchronization on these methods is used solely for its communication effects, not for mutual exclusion.

你认为它工作的原因是因为在没有同步的情况下,当线程本地内存中的值被刷新到主内存时,JVM 没有“保证” ,这意味着它可能根本不刷新或者可能刷新但不能保证 "when" 。当你 运行 它时,值就会被刷新,但它不一定总是被刷新,所以如果你使用同步(或 volatile,取决于场景),这就是 "guarantee" 出现的地方然后 JVM 保证“happens-before”关系,这不过是保证将值从线程本地内存刷新到主内存 "happen before" 任何线程都可以从中读取值主内存。

在没有同步的情况下检查读写相关并发问题的影响的一个更好的例子可能是流行的银行账户借方贷方示例,下面是快速示例:

public class AccountDebitCredit {

    private int accountBalance = 100;

    public static void main(String[] args) throws InterruptedException {
        final AccountDebitCredit accountDebitCredit = new AccountDebitCredit();

        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    // if you remove synchronization from t1 and t2, then there would be concurrency issues.
                    synchronized (accountDebitCredit) {
                        accountDebitCredit.accountBalance = accountDebitCredit.accountBalance + 100;
                    }
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {

            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    // if you remove synchronization from t1 and t2, then there would be concurrency issues.
                    synchronized (accountDebitCredit) {
                        accountDebitCredit.accountBalance = accountDebitCredit.accountBalance - 100;
                    }
                }
            }
        });

        System.out.println(accountDebitCredit.accountBalance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(accountDebitCredit.accountBalance);
    }

}