Несинхронизированные i ++ перекрываются? - PullRequest
0 голосов
/ 07 октября 2019

Попытка получить основы многопоточности Java Я столкнулся с делом, которое не могу понять. Сообщество, поделитесь своим опытом, почему это происходит: у меня есть Runnable:

class ImproperStateWorker implements Runnable {
    private int val = 0;
    @Override
    public void run() {
        //Where are missing ticks?
        for (int i = 0; i < 1000; i++) {
                    val++;
                    Thread.yield();
                    val++;
        }
        showDataState();
    }

    public void showDataState() {
        System.out.print(Thread.currentThread() + " ; ");
        System.out.println(val);
    }
}

, который запускается через:

public class ImproperState {
public static void main(String[] args) {
    ImproperStateWorker worker = new ImproperStateWorker();
    for (int i = 0; i < 2; i++) {
        Thread thread = new Thread(worker);
        thread.start();
    }
}

}

Я понимаю, чтоИдея состоит в том, чтобы сделать обе операции приращения атомарными, используя synchronized() {...}, и так далее. Но меня смущает следующее: почему запуск этих двух Runnables (без синхронизации) не дает последовательного результата 4000 (1000 x 2 приращений на задачу)? Независимо от того, как контекст переключается между двумя задачами, я ожидаю, что задачи будут выполнять по 2000 шагов, мне все равно, какой будет порядок.

Однако вывод программы дает ~ 3.5K. Единственная идея, о которой я могу подумать, заключается в том, что «отсутствующие» приращения возникают потому, что некоторые из них создаются одновременно, так что val++, вызываемый из двух потоков, фактически увеличивает значение на 1. Но это очень смутное предположение. Спасибо, что поделились своим опытом.

Ответы [ 2 ]

3 голосов
/ 07 октября 2019

У вас есть условие гонки в вашем коде. Рассмотрим следующие возможные чередования:

  1. Поток 1 : чтение val0
  2. Поток 1 : приращение 01
  3. Тема 1 : запись val1
  4. Тема 1 : чтение val1
  5. нить 1 : приращение 12
  6. нить 1 : запись val2
  7. Резьба 2 : чтение val2
  8. Резьба 2 : приращение 23
  9. Тема 2 : запись val3
  10. Тема 2 : чтение val3
  11. Тема 2: приращение 34
  12. Нить 2 : запись val4
  13. Конечное состояние: val == 4

Все хорошо. Но рассмотрим это в равной степени возможное чередование:

  1. Поток 1 : читает val0
  2. Поток 2 : читаетval0
  3. Нить 1 : приращение 01
  4. Нить 2 : приращение 01
  5. Тема 1 : запись val1
  6. Тема 2 : запись val1
  7. Тема 1 : чтение val1
  8. Тема 2 : чтение val1
  9. нить 1 : приращение 12
  10. нить 2 : приращение 12
  11. Поток 1 : запись val2
  12. Поток 2 : запись val2
  13. Конечное состояние: val == 2

Упс!

В коде, как написано в вашем вопросе, результат может быть что угодно между 2000 и 4000.

Один из способов исправить это - использовать AtomicInteger с AtomicInteger.getAndIncrement() или AtomicInteger.incrementAndGet() (в вашем случае не имеет значения, какой, поскольку вы игнорируете возвращаемое значение;вместо этого правильный эквивалент постфикса val++ будет val.getAndIncrement()), например так (вам нужно изменить только три места, не считая import.):

import java.util.concurrent.atomic.AtomicInteger;

class FixedStateWorker implements Runnable {
    private AtomicInteger val = new AtomicInteger();
    //      ↑↑↑↑↑↑↑↑↑↑↑↑↑       ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            val.getAndIncrement();
            //  ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
            Thread.yield();
            val.getAndIncrement();
            //  ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
        }
        showDataState();
    }
}
0 голосов
/ 08 октября 2019

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...