Извините, что такой длинный вопрос.
В последнее время я много изучал многопоточность, постепенно внедряя ее в личный проект. Однако, наверное из-за обилия немного некорректных примеров, использование синхронизированных блоков и волатильности в тех или иных ситуациях мне все же немного непонятно.
Мой основной вопрос заключается в следующем: являются ли изменения ссылок и примитивов автоматически изменчивыми (то есть выполняются в основной памяти, а не в кэше), когда поток находится внутри синхронизированного блока, или чтение также должно быть синхронизировано для это работать правильно?
(обратите внимание на следующие примеры, которые я знаю, что синхронизированные методы и синхронизированные (это) не одобряются и почему, но обсуждение этого выходит за рамки моего вопроса)
Пример 1:
class Counter{
int count = 0;
public synchronized void increment(){
count++;
}
public int getCount(){
return count;
}
}
В этом примере increment() необходимо синхронизировать, поскольку ++ не является атомарной операцией. Таким образом, одновременное увеличение двух потоков может привести к общему увеличению счетчика на 1.Примитив count должен быть атомарным (например, не длинным/двойным/ссылочным), и это нормально.
Нужно ли здесь синхронизировать getCount() и почему именно? Объяснение, которое я слышал чаще всего, заключается в том, что у меня не будет гарантии, будет ли возвращенный счет до или после приращения. Однако это похоже на объяснение чего-то немного другого, которое оказалось не в том месте. Я имею в виду, что если бы я синхронизировал getCount(), то я все еще не вижу никакой гарантии - теперь все сводится к незнанию порядка блокировки, вместо того, чтобы не знать, происходит ли фактическое чтение до/после фактической записи.
Пример 2:
Является ли следующий пример потокобезопасным, если предположить, что из-за не показанного здесь обмана ни один из этих методов никогда не будет вызываться одновременно? Будет ли считаться приращение ожидаемым образом, если это делается с использованием случайного метода каждый раз, а затем считывается правильно, или блокировка должна быть одним и тем же объектом? (кстати, я полностью понимаю, насколько смехотворен этот пример, но меня больше интересует теория, чем практика)
class Counter{
private final Object lock1 = new Object();
private final Object lock2 = new Object();
private final Object lock3 = new Object();
int count = 0;
public void increment1(){
synchronized(lock1){
count++;
}
}
public void increment2(){
synchronized(lock2){
count++;
}
}
public int getCount(){
synchronized(lock3){
return count;
}
}
}
Пример 3:
Является ли отношение «происходит до» просто концепцией Java, или это реальная вещь, встроенная в JVM? Несмотря на то, что я могу гарантировать концептуальное отношение «происходит до» для следующего примера, достаточно ли умна java, чтобы подобрать его, если это встроенная вещь? Я предполагаю, что это не так, но действительно ли этот пример потокобезопасен? Если это потокобезопасно, как насчет того, чтобы getCount() не блокировался?
class Counter{
private final Lock lock = new Lock();
int count = 0;
public void increment(){
lock.lock();
count++;
lock.unlock();
}
public int getCount(){
lock.lock();
int count = this.count;
lock.unlock();
return count;
}
}