Основная проблема с потоками Java - PullRequest
4 голосов
/ 03 декабря 2009

Допустим, я взаимодействую с системой, в которой есть два увеличивающихся счетчика, которые зависят друг от друга (эти счетчики никогда не уменьшатся): int totalFoos; // barredFoos плюс nonBarredFoos int barredFoos;

У меня также есть два метода: int getTotalFoos (); // В основном сетевой вызов localhost int getBarredFoos (); // В основном сетевой вызов localhost

Эти два счетчика сохраняются и увеличиваются кодом, к которому у меня нет доступа. Предположим, что он увеличивает оба счетчика в альтернативном потоке, но потокобезопасным способом (т. Е. В любой момент времени оба счетчика будут синхронизированы).

Каков наилучший способ получить точное количество как barredFoos, так и nonBarredFoos в один момент времени?

Совершенно наивная реализация:

int totalFoos = getTotalFoos();
int barredFoos = getBarredFoos();
int nonBarredFoos = totalFoos - barredFoos;

Это связано с тем, что система может увеличивать оба счетчика между двумя вызовами методов, и тогда мои две копии будут не синхронизированы, и значение barredFoos будет больше, чем при получении totalFoos.

Базовая реализация с двойной проверкой:

while (true) {
    int totalFoos = getTotalFoos();
    int barredFoos = getBarredFoos();

    if (totalFoos == getTotalFoos()) {
        // totalFoos did not change during fetch of barredFoos, so barredFoos should be accurate.
        int nonBarredFoos = totalFoos - barredFoos;
        break;
    }

    // totalFoos changed during fetch of barredFoos, try again
}

Теоретически это должно работать, но я не уверен, что JVM гарантирует, что это то, что на самом деле происходит на практике после оптимизации, и это принимается во внимание. Пример этих проблем приведен в http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html (ссылка через Ромена Мюллера).

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

Ответы [ 6 ]

3 голосов
/ 03 декабря 2009

Да, я считаю, что ваша реализация будет достаточной; настоящая работа заключается в том, чтобы убедиться, что значения, возвращаемые getTotalFoos и getBarredFoos, действительно синхронизированы и всегда возвращают последние значения. Однако, как вы сказали, это уже так.

Конечно, одна вещь, с которой вы можете столкнуться с этим кодом, - это бесконечный цикл; Вы хотели бы быть уверены, что эти два значения, изменяемые за такое короткое время, были бы очень исключительной ситуацией, и даже тогда я думаю, что было бы определенно разумно встроить безопасность (т.е. максимальное количество итераций), чтобы избежать получения в бесконечный цикл. Если значение, полученное из этих счетчиков, находится в коде, к которому у вас нет доступа, вы не хотите полностью полагаться на тот факт, что на другом конце все никогда не пойдет не так.

1 голос
/ 03 декабря 2009

Чтобы гарантировать согласованность чтения между потоками и предотвратить изменение порядка выполнения кода, особенно на многоядерных компьютерах, вам необходимо синхронизировать все права на чтение и запись для этих переменных. Кроме того, чтобы убедиться, что в одном потоке вы видите самые актуальные значения всех переменных, используемых в текущем вычислении, вам необходимо синхронизировать доступ на чтение.

Обновление: Я пропустил бит о вызовах, чтобы получить значения обеих переменных, являющихся отдельными вызовами по сети, - что делает эту проблему двойной проверкой блокировки (поэтому без доступного вам метода API который возвращает оба значения одновременно, вы не можете абсолютно гарантировать согласованность обеих переменных в любой момент времени).

См. Статью Брайана Гетца о Модель памяти Java .

0 голосов
/ 03 декабря 2009

Метод, содержащий код

while (true) {    
 int totalFoos = getTotalFoos();    
 int barredFoos = getBarredFoos();    
 if (totalFoos == getTotalFoos()) {        
  int nonBarredFoos = totalFoos - barredFoos;        
  break;    
 } 

}

Должен быть синхронизирован

private synchronized void getFoos()
0 голосов
/ 03 декабря 2009

Учитывая, что нет никакого способа получить доступ к какому-либо механизму блокировки, поддерживающему инвариант «totalFoos = barredFoos + nonBarredFoos», невозможно гарантировать, что полученные значения соответствуют этому инварианту. К сожалению.

0 голосов
/ 03 декабря 2009

Я собирался упомянуть и AtomicInteger, но это не сработает, потому что

1) у вас есть ДВА целых числа, а не только одно. AtomicIntegers вам не помогут. 2) У него нет доступа к базовому коду.

Мой вопрос: даже если вы не можете изменить базовый код, можете ли вы контролировать , когда выполняется? Вы можете поместить блоки синхронизации вокруг любых функций, которые изменяют эти счетчики. Это может быть не очень приятно (это может быть медленнее, чем ваш цикл), но, возможно, это будет «правильный» способ сделать это.

Если вы даже не можете контролировать внутренние потоки, тогда, я думаю, ваш цикл будет работать.

И, наконец, если вы когда-нибудь получите контроль над кодом, лучше всего было бы иметь одну синхронизированную функцию, которая блокирует доступ к обоим целым числам во время ее выполнения, и возвращает их в int [].

0 голосов
/ 03 декабря 2009

Вы можете , вероятно, ненадежно делать то, что вы хотите, если только система, с которой вы взаимодействуете, не имеет метода, который позволяет вам получить оба значения одновременно (атомарным способом).

...