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

时间:2016-02-29 02:36:04

标签: java multithreading java-memory-model

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

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

  1. 读者线程看到的值永远不会减少。
  2. 读取最终是一致的。经过一段时间没有任何写入后,所有读取都将返回准确的值。
  3. 假设编写者彼此正确同步,是否有必要将读者线程与编写者同步以保证正确性,如上所述?

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

    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.
            }
        }
    }
    

2 个答案:

答案 0 :(得分:3)

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

如另一个答案中所述,在您当前的示例中,“最终计数”将是正确的,因为主线程正在加入编写器线程(从而建立发生在之前的关系)。但是,您的读者线程永远不能保证看到计数的任何更新。

答案 1 :(得分:1)

JTahlborn is correct, +1 from me.我匆匆忙忙地误读了这个问题,我误以为读者线程是主线。

由于the happens-before relationship

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

线程中的所有操作都会发生 - 在任何其他线程从该线程的连接成功返回之前。

一旦主线程连接到所有编写器,则计数器的更新值可见。然而,没有发生在之前的关系强迫读者的观点得到更新,你受JVM实现的支配。如果有足够的时间过去,JLS中没有关于值可见的承诺,它对实现保持开放。计数器值可能会被缓存,读者可能无法看到任何更新。

在一个平台上进行测试并不能保证其他平台会做什么,所以不要因为测试在你的PC上传递而认为这没关系。我们中有多少人在我们部署的同一平台上开发?

在柜台上使用volatile或使用AtomicInteger将是很好的修复。使用AtomicInteger将允许从编写器线程中删除锁。使用volatile而不锁定只有在只有一个编写器的情况下才会出现,当存在两个或多个编写器时,++或+ =不是线程安全将是一个问题。使用Atomic类是更好的选择。

(顺便说一句,吃掉InterruptedException并不是“安全”,它只是让线程无法响应中断,当你的程序要求线程提前完成时就会发生这种情况。)