synchronized
само по себе означает, что если несколько потоков пытаются запустить этот фрагмент кода одновременно, только один из этих потоков может быть разрешен внутри блока в любой момент времени. synchronized (listeners)
использует listeners
в качестве идентификатора блокировки, что означает, что это ограничение применяется ко всем блокам, которые синхронизируются с этой переменной - если один поток находится внутри одного из этих блоков, никакой другой поток не может войти ни в один из них.
Даже если в блоке есть только один вызов функции, это может иметь смысл: эта функция состоит из множества других инструкций, и элемент управления может переключаться на другой поток, в то время как первый находится в середине этой функции. , Если функция не поточно-безопасная , это может вызвать проблемы, такие как перезапись данных.
В этом конкретном случае вызов функции состоит из добавления значения в коллекцию listeners
. Хотя создание потоково-безопасных коллекций не является невозможным, большинство коллекций не являются поточно-ориентированными для нескольких авторов Таким образом, чтобы гарантировать, что коллекция не испортилась, необходимо synchronized
.
РЕДАКТИРОВАТЬ: Чтобы дать пример того, как все может запутаться, предположим, что это упрощенная реализация add
, где length
- это количество элементов в массиве items
:
public void Add(T item) {
items[length++] = item;
}
Этот length++
бит не является атомарным; он состоит из чтения, приращения и записи, и поток может быть прерван после любого из них. Итак, давайте перепишем это немного, чтобы увидеть, что на самом деле происходит:
public void Add(T item) {
int temp = length;
length = length + 1;
items[temp] = item;
}
Теперь предположим, что два потока T1 и T2 вводят одновременно. Вот один из возможных наборов событий:
T1: int temp = length;
T2: int temp = length;
T2: length = length + 1;
T2: items[temp] = item;
T1: length = length + 1;
T1: items[temp] = item;
Проблема в том, что одно и то же значение используется для temp
обоими потоками, поэтому последний оставленный поток в итоге перезаписывает элемент, который был помещен туда первым; и в самом конце есть неназначенный предмет.
Также не помогает, если length
представляет следующий индекс, который будет использоваться, поэтому мы можем использовать предварительный индекс:
public void Add(T item) {
items[++length] = item;
}
Опять переписываем это:
public void Add(T item) {
length = length + 1;
items[length] = item;
}
Теперь это возможная последовательность событий:
T1: length = length + 1;
T2: length = length + 1;
T2: items[length] = item;
T1: items[length] = item;
Еще раз, последний поток завершает перезапись первого, но теперь неназначенный элемент является вторым по последнему элементу.