Несинхронизированные чтения (в сочетании с синхронизированной записью) в конечном итоге согласованы

У меня есть вариант использования со многими потоками писателей и одним потоком чтения. Записываемые данные - это счетчик событий, который считывается потоком отображения.

Счетчик только увеличивается, а дисплей предназначен для людей, поэтому точное значение момента времени не имеет решающего значения. Для этой цели я считаю решение правильным, если:

  1. Ценность, которую видит читающий поток, никогда не уменьшается.
  2. Показания в конечном итоге стабильны. По прошествии определенного времени без записи все операции чтения вернут точное значение.

Предполагая, что писатели правильно синхронизированы друг с другом, необходимо ли синхронизировать поток чтения с писателями, чтобы гарантировать правильность, как определено выше?

Упрощенный пример. Было бы это правильно, как определено выше?

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

person DMunz    schedule 29.02.2016    source источник
comment
Я удалил вызов getCount () после присоединения к потокам, так как это немного запутало вопрос.   -  person DMunz    schedule 29.02.2016


Ответы (2)


Нет никакой гарантии, что читатели когда-либо увидят обновление счетчика. Простое решение - сделать счетчик непостоянным.

Как отмечено в другом ответе, в вашем текущем примере «Окончательный счет» будет правильным, потому что основной поток присоединяется к потокам записи (таким образом устанавливая связь «происходит до»). однако никогда не гарантируется, что ваш читательский поток увидит какое-либо обновление счетчика.

person jtahlborn    schedule 29.02.2016
comment
Точно. Компилятору даже разрешено вытаскивать sharedCounter.getCount() из цикла в потоке чтения и назначать его временной переменной. Вы не гарантируете, что увидите какие-либо обновления в цепочке читателей. - person Erwin Bolwidt; 29.02.2016
comment
@ErwinBolwidt - нет никаких гарантий, даже если счетчик является непостоянным .... JMM позволяет объединять последовательные изменчивые чтения. однако ни одна разумная JVM этого не делает. - person ZhongYu; 29.02.2016
comment
Когда вы говорите, что нет гарантии, что читатели когда-либо увидят обновление счетчика, вы имеете в виду, что читатели могут пропустить отдельные обновления, или вы имеете в виду, что читатели могут пропустить все обновления (т.е. продолжать получать 0 бесконечно)? - person DMunz; 29.02.2016
comment
@ bayou.io - если то, что вы утверждаете, практически актуально, то я не вижу параллельного кода, когда-либо работающего в jvm. этот поток, который вы связали, является очень теоретическим (я прошел только половину). это не имеет никакого значения ни в одной известной мне реализации jvm. - person jtahlborn; 29.02.2016
comment
@jtahlborn - правда. но он показывает неадекватность того, как формулируется JMM, если он не решает подобные проблемы. Я слышал, что они переписывают JMM. - person ZhongYu; 29.02.2016
comment
@ bayou.io - это первый раз, когда я слышал о переписывании JMM. но, тем не менее, я думаю, что мой ответ верен для любой современной реализации jvm. - person jtahlborn; 29.02.2016

JTahlborn прав, +1 от меня. Я торопился и неправильно прочитал вопрос, я ошибочно предположил, что поток читателя был основной поток.

Основной поток может правильно отображать окончательный счет благодаря связь "происходит до":

Все действия в потоке происходят до того, как любой другой поток успешно вернется из соединения в этом потоке.

Как только основной поток присоединяется ко всем писателям, отображается обновленное значение счетчика. Тем не менее, не существует отношения «произошло до», заставляющего взгляд читателя обновляться, вы находитесь во власти реализации JVM. В JLS нет обещания о том, что значения станут видимыми, если пройдет достаточно времени, это остается открытым для реализации. Значение счетчика могло быть кэшировано, и читатель мог вообще не видеть никаких обновлений.

Тестирование этого на одной платформе не дает уверенности в том, что будут делать другие платформы, поэтому не думайте, что это нормально только потому, что тест проходит на вашем ПК. Многие ли из нас разрабатывают ту же платформу, на которой разворачиваются?

Использование volatile на счетчике или использование AtomicInteger было бы хорошим исправлением. Использование AtomicInteger позволит снять блокировки с потока записи. Использование volatile без блокировки будет приемлемым только в том случае, если есть только один писатель, когда присутствуют два или более писателя, тогда ++ или + =, не являющиеся потокобезопасными, будут проблемой. Лучше использовать атомарный класс.

(Между прочим, употребление InterruptedException не является «безопасным», оно просто делает поток невосприимчивым к прерыванию, что происходит, когда ваша программа просит поток завершить работу раньше.)

person Nathan Hughes    schedule 29.02.2016
comment
хотя это правда, я не думаю, что цель вопроса - это окончательный подсчет, а то, что видит читатель. - person jtahlborn; 29.02.2016