非同步读取(结合同步写入)是否最终一致

Are unsynchronized reads (combined with synchronized writes) eventually consistent

我有一个包含许多编写器线程和一个 reader 线程的用例。正在写入的数据是一个事件计数器,正在由显示线程读取。

计数器只会增加并且显示是供人类使用的,因此确切的时间点值并不重要。为此,我认为一个解决方案是正确的,只要:

  1. reader 线程看到的值永远不会减少。
  2. 读取最终是一致的。在一定时间没有任何写入后,所有读取都将 return 精确值。

假设编写器彼此正确同步,是否有必要将 reader 线程与编写器同步以保证正确性,如上定义?

一个简化的例子。如上定义,这是否正确?

public class Eventual {

    private static class Counter {
        private int count = 0;
        private Lock writeLock = new ReentrantLock();

        // Unsynchronized reads
        public int getCount() {
            return count;
        }

        // Synchronized writes
        public void increment() {
            writeLock.lock();
            try {
                count++;
            } finally {
                writeLock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        List<Thread> contentiousThreads = new ArrayList<>();
        final Counter sharedCounter = new Counter();

        // 5 synchronized writer threads
        for(int i = 0; i < 5; ++i) {
            contentiousThreads.add(new Thread(new Runnable(){
                @Override
                public void run() {
                    for(int i = 0; i < 20_000; ++i) {
                        sharedCounter.increment();
                        safeSleep(1);
                    }
                }
            }));
        }

        // 1 unsynchronized reader thread
        contentiousThreads.add(new Thread(new Runnable(){
            @Override
            public void run() {
                for(int i = 0; i < 30; ++i) {
                    // This value should:
                    //   +Never decrease 
                    //   +Reach 100,000 if we are eventually consistent.
                    System.out.println("Count: " + sharedCounter.getCount());
                    safeSleep(1000);
                }
            }
        }));

        contentiousThreads.stream().forEach(t -> t.start());

        // Just cleaning up...
        // For the question, assume readers/writers run indefinitely
        try {
            for(Thread t : contentiousThreads) {
                t.join();
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void safeSleep(int ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            //Don't care about error handling for now.
        }
    }
}

急着看错了问题,我误以为 reader 线程是主线程。

由于the happens-before relationship:

主线程可以正确显示最终计数

All actions in a thread happen-before any other thread successfully returns from a join on that thread.

一旦主线程加入了所有写入器,计数器的更新值就可见了。但是,没有强制 reader 的视图更新的发生前关系,您将受制于 JVM 实现。如果经过足够长的时间,JLS 中没有关于值变得可见的承诺,它留待实现。计数器值可能会被缓存,并且 reader 可能看不到任何更新。

在一个平台上测试这个并不能保证其他平台会做什么,所以不要因为测试在您的 PC 上通过就认为这没问题。我们中有多少人在部署到的同一平台上进行开发?

在计数器上使用 volatile 或使用 AtomicInteger 将是很好的修复方法。使用 AtomicInteger 将允许从编写器线程中删除锁。只有在只有一个编写器的情况下才可以在没有锁定的情况下使用 volatile,当存在两个或多个编写器时,++ 或 += 不是线程安全的将是一个问题。使用 Atomic class 是更好的选择。

(顺便说一下,吃掉 InterruptedException 不是 "safe",它只是让线程对中断没有响应,当您的程序要求线程提前完成时会发生这种情况。)

无法保证 reader 会看到计数更新。一个简单的解决方法是使计数不稳定。

如另一个答案所述,在您当前的示例中,"Final Count" 将是正确的,因为主线程正在加入编写器线程(因此建立了先行关系)。但是,您的 reader 线程永远无法保证看到计数的任何更新。