Насколько известно, изменив volatile переменную, мы остаёмся уверенны, что остальные потоки, которые будут её читать, получат новое значение. Причина тому ясна: значения переменных не кэшируются (конечно, там больше происходящего, но сейчас важна суть). Так вот мне стало неясно, а что происходит с видимостью изменений non-volatile переменных при синхронизации?
В достоверном источнике сказано следующее:
Синхронизация вынуждает запись данных происходить в основную память, и если поле полностью защищено синхронизированными методами или блоками, объявлять его volatile для параллельного доступа не обязательно.
Так вот, вопрос: а как это случается, почему же ключевое слово volatile нам теперь не нужно? Нам нужны synchronized getter'ы и setter'ы, али же достаточно синхронизировано прочитать? А может только записать?
Поясню часть вопроса про getter'ы и setter'ы:
Пусть есть код:
class A {
int n=1;
public void change(){
while (!Thread.interrupted())
synchronized (this){
n=3*n+4;
}
}
}
в main(String[] args) того же пакета, что и класс A:
A a = new A();
new Thread(a::change).start();
TimeUnit.SECONDS.sleep(10);
System.out.println(a.n);
И здесь возникает вопрос.
Давайте сейчас забудем про то, что переменная n могла переполниться, а потом через наш sleep в 10 секунд снова стать равной 1. И забудем про то, что когда мы читаем n она может быть в некотором "неустойчивом" состоянии.
Так вот, достаточно ли было синхронизации в методе change()? Гарантирует ли она проталкивание в main memory нашей переменной n? Т.е. по факту вопрос такой: правда ли, что не будет такого, что через 10 секунд сна доставая a.n мы получим там 1, ибо значение этого поля пока лежит в кэше?
А если изменить последнюю строчку в main(String[] args) на:
synchronized (a){
System.out.println(a.n);
}
Теперь точно всё хорошо и проблем с видимостью изменений не будет? Как так устроен этот аспект механизма синхронизации? Т.е. change() не проталкивал в main memory, а при синхронизированном чтении сразу проталкивалось всё?... Или синхронизации в change() было достаточно?
В приложение к ответу буду очень благодарен увидеть цитаты из документации (с переводом на русский язык) к Java с ссылкой на них.
Здеся и тута есть важные мысли, но я хочу получить ответ на всё-таки другой вопрос.
Модификаторы volatile и synchronized решают ведь не только проблему с кэшированием. Есть ещё масса нюансов, которые требуется учитывать в многопоточном программировании. Например, reordering. Есть ряд случаев, в которых доступ к переменным (полям объектов, статическим полям и элементам массива) может выполниться в порядке, отличном от указанного в программе. Компилятор свободен в расположении инструкций с целью оптимизации. Процессоры могут выполнять инструкции в ином порядке в ряде случаев. Данные могут перемещаться между регистрами, кэшами процессора и оперативной памятью в порядке, отличном от указанного в программе. Но JMM гарантирует сохранение отношений happens-before внутри синхронизированных блоков, устанавливая барьер при захвате и освобождении блокировки. Сама семантика слова "синхронизация" говорит за себя. Поток осуществляющий синхронизированный доступ к переменной, синхронизирует своё состояние. Но действительно актуальное состояние переменной для всех возможно только в том случае, если синхронизируются все.
Кроме того, не стоит забывать, что синхронизация используется не только для решения проблемы видимости, но и атомарности. Если один поток захватывает блокировку при доступе к полю, а другой даже не пытается её захватить и, соответственно, проверить её состояние, то толку от блокировки нет, и программа испортит состояние.
P.S. Вместо ссылок на документацию приведу ссылки на доклады человека, чьё имя стало нарицательным в этой области.
Aleksey Shipilёv - Java Memory Model Pragmatics, part 1
Aleksey Shipilёv - Java Memory Model Pragmatics, part 2
Алексей Шипилёв — Близкие Контакты JMM-степени
Продвижение своими сайтами как стратегия роста и независимости