Почему мои потоки выдают мне этот вывод при доступе к синхронизированному методу?
Я пытаюсь поэкспериментировать с многопоточностью и синхронизацией, поэтому я создал этот простой объект, который используется всеми потоками:
public class SharedObject {
private int count = 0;
public synchronized int getCount(){
return count;
}
public synchronized void incrementCount(){
count++;
}
}
И к нему получают доступ 3 потока следующим образом:
public static void main(String[] args) throws Exception {
SharedObject sharedObject = new SharedObject();
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(3);
Runnable task = () -> {
for(int i = 0; i < 10; i++){
System.out.println("Thread : " + Thread.currentThread().getName()
+ " count : " + sharedObject.getCount());
sharedObject.incrementCount();
try{
Thread.currentThread().sleep(2000);
}
catch (Exception e){}
}
};
executor.submit(task);
executor.submit(task);
executor.submit(task);
executor.shutdown();
executor.awaitTermination(1, TimeUnit.HOURS);
System.out.println("Final : " + sharedObject.getCount());
}
Мой вывод следующий:
Thread : pool-1-thread-2 count : 0
Thread : pool-1-thread-1 count : 0
Thread : pool-1-thread-3 count : 0
Thread : pool-1-thread-3 count : 3
Thread : pool-1-thread-2 count : 3
Thread : pool-1-thread-1 count : 3
Thread : pool-1-thread-2 count : 6
Thread : pool-1-thread-1 count : 6
Thread : pool-1-thread-3 count : 6
...
Если мое понимание верно (пожалуйста, поправьте меня, если я не прав), это происходит потому, что:
Первый поток вызывает
getCount()
, получает блокировку для метода и, как только он печатает значение счетчика, освобождает блокировку, которую затем получает второй поток для вызоваgetCount()
и далее с последней темойКогда все 3 потока закончили звонить
getCount()
каждый из них сейчас звонитincrementCount()
и поскольку метод синхронизирован, каждый поток видит обновленное значение перед увеличением счетчика, что объясняет, почему мы видим скачки +3 на выходеКак только поток заканчивается, он вызывает
sleep(2000)
сам по себе, но так как вызовы очень быстрые, кажется, что три потока запускаются и останавливаются спать одновременно
Тем не менее, когда я удаляю sleep(2000)
я получаю следующий вывод:
Thread : pool-1-thread-3 count : 0
Thread : pool-1-thread-2 count : 0
Thread : pool-1-thread-1 count : 0
Thread : pool-1-thread-2 count : 2
Thread : pool-1-thread-3 count : 1
Thread : pool-1-thread-2 count : 4
Thread : pool-1-thread-1 count : 3
Thread : pool-1-thread-2 count : 6
Thread : pool-1-thread-3 count : 5
Thread : pool-1-thread-2 count : 8
Thread : pool-1-thread-1 count : 7
Thread : pool-1-thread-2 count : 10
Thread : pool-1-thread-3 count : 9
Thread : pool-1-thread-2 count : 12
Thread : pool-1-thread-1 count : 11
Thread : pool-1-thread-2 count : 14
И я не понимаю, как это может произойти. Например, как можно thread-3
увидеть количество равным 1, если thread-2
видел его равным 2 до него и увеличивал его?
Любое объяснение будет полезно, чтобы помочь мне лучше понять Java в многопоточной синхронизированной среде. Спасибо за ваше время.
3 ответа
Помните, что вывод на вашем экране может не иметь никакого отношения к порядку выполнения getCount()/incrementCount(), в коде, который печатает вывод, нет блокировки.
Например, как поток-3 может видеть счет равным 1, если поток-2 видел его равным 2 перед ним и увеличивал его?
Этот вывод может произойти, если у вас есть этот порядок выполнения:
- Поток-3 вызывает getCount () и возвращает 1.
- Поток-1 вызывает incrementCount ()
- Поток-2 вызывает getCount () и возвращает 2.
- Поток-2 вызывает System.out.println, который печатает: "pool-1-thread-2 count: 2"
- Поток-3 вызывает System.out.println, который печатает: "счет пула-1-поток-3: 1"
То, что поток читает значение раньше другого, не означает, что он будет печатать его раньше другого. Вам понадобится атомное чтение и печать внутри синхронизированного блока, чтобы иметь такую гарантию.
Так что вы можете иметь, таким образом,
поток 3 читает и печатает значение (0): поток: счетчик пула-1-поток-3: 0
поток 2 читает и печатает значение (0): поток: счетчик пула-1-поток-2: 0
поток 1 читает и печатает значение (0): поток: пул-1-поток-1 счетчик: 0
поток 3 увеличивает значение (1)
- поток 3 читает значение (1)
- поток 2 увеличивает значение (2)
- поток 2 читает и печатает значение (2): поток: пул-1-поток-2 счетчик: 2
- поток 3 печатает значение, которое он прочитал ранее: поток: пул-1-поток-3 счетчик: 1
В вашем SharedObject
, человек getCount
а также incrementCount
методы синхронизированы, но ничто не мешает вызову всех трех потоков getCount
(по одному), прежде чем кто-либо из них звонит incrementCount
, А потом снова после каждого сна. Это то, что показывает ваш первый вывод.
Без sleep()
кроме того, для одного потока возможно вызвать getCount()
более одного раза, прежде чем один или несколько других звонков incrementCount()
даже однажды. Технически это не запрещено даже во сне. Аналогично, один поток может получать, увеличивать и печатать промежуточные значения, когда другой получает и когда он печатает. Эти виды заказов объясняют, почему ваш вывод без sleep
не является последовательным
Если вы хотите видеть последовательный вывод без пропусков, то вам нужна более широкая синхронизация. Например, в вашей реализации задачи вы можете использовать synchronized
блок:
synchronized (sharedObject) {
System.out.println("Thread : " + Thread.currentThread().getName()
+ " count : " + sharedObject.getCount());
sharedObject.incrementCount();
}
Каждый раз, когда поток вводит synchronized
блок, он получит счетчик, распечатает его и увеличит его без каких-либо других потоков между ними.