简体   繁体   中英

Are unsynchronized reads (combined with synchronized writes) eventually consistent

I have a use case with many writer threads and a single reader thread. The data being written is an event counter which is being read by a display thread.

The counter only ever increases and the display is intended for humans, so the exact point-in-time value is not critical. For this purpose, I would consider a solution to be correct as long as:

  1. The value seen by the reader thread never decreases.
  2. Reads are eventually consistent. After a certain amount of time without any writes, all reads will return the exact value.

Assuming writers are properly synchronized with each other, is it necessary to synchronize the reader thread with the writers in order to guarantee correctness, as defined above?

A simplified example. Would this be correct, as defined above?

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

There is no guarantee that the readers would ever see an update to the count. A simple fix is to make count volatile.

As noted in another answer, in your current example, the "Final Count" will be correct because the main thread is joining the writer threads (thus establishing a happens-before relationship). however, your reader thread is never guaranteed to see any update to the count.

JTahlborn is correct, +1 from me. I was rushing and misread the question, I was assuming wrongly that the reader thread was the main thread.

The main thread can display the final count correctly due to the happens-before relationship :

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

Once the main thread has joined to all the writers then the counter's updated value is visible. However, there is no happens-before relationship forcing the reader's view to get updated, you are at the mercy of the JVM implementation. There is no promise in the JLS about values getting visible if enough time passes, it is left open to the implementation. The counter value could get cached and the reader could possibly not see any updates whatsoever.

Testing this on one platform gives no assurance of what other platforms will do, so don't think this is OK just because the test passes on your PC. How many of us develop on the same platform we deploy to?

Using volatile on the counter or using AtomicInteger would be good fixes. Using AtomicInteger would allow removing the locks from the writer thread. Using volatile without locking would be OK only in a case where there is just one writer, when two or more writers are present then ++ or += not being threadsafe will be an issue. Using an Atomic class is a better choice.

(Btw eating the InterruptedException isn't "safe", it just makes the thread unresponsive to interruption, which happens when your program asks the thread to finish early.)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM