Нет, это не потокобезопасно. invoc должен быть объявлен как volatile, или доступен при синхронизации с той же блокировкой, или изменен для использования AtomicInteger. Просто использовать синхронизированный метод для увеличения invoc, но не синхронизировать его для чтения, недостаточно.
JVM выполняет много оптимизаций, включая кэширование для конкретного процессора и переупорядочение команд. Он использует ключевое слово volatile и блокировку, чтобы решить, когда он может свободно оптимизировать и когда ему нужно иметь актуальное значение, доступное для чтения другими потоками. Поэтому, когда читатель не использует блокировку, JVM не может знать, не присвоить ли ей устаревшее значение.
В этой цитате из Java-параллелизма на практике (раздел 3.1.3) обсуждается, как синхронизация записи и чтения должна быть синхронизирована:
Внутренняя блокировка может использоваться, чтобы гарантировать, что один поток видит эффекты другого в предсказуемой манере, как показано на рисунке 3.1. Когда поток A выполняет синхронизированный блок, а затем поток B входит в синхронизированный блок, защищенный той же самой блокировкой, значения переменных, которые были видны A до снятия блокировки, гарантированно будут видны B после получения блокировки. Другими словами, все, что A делал в или до синхронизированного блока, видимо B, когда он выполняет синхронизированный блок, защищенный той же блокировкой. Без синхронизации такой гарантии нет.
Следующий раздел (3.1.4) описывает использование volatile:
Язык Java также предоставляет альтернативную, более слабую форму синхронизации, изменчивых переменных, чтобы гарантировать, что обновления переменной распространяются предсказуемо в другие потоки. Когда поле объявляется как volatile, компилятор и среда выполнения уведомляются о том, что эта переменная является общей и что операции с ней не должны переупорядочиваться с другими операциями с памятью. Энергозависимые переменные не кэшируются в регистрах или в кэшах, где они скрыты от других процессоров, поэтому чтение энергозависимой переменной всегда возвращает самую последнюю запись любым потоком.
Вернемся к тому моменту, когда у всех наших компьютеров были однопроцессорные машины, мы писали код и никогда не сталкивались с проблемами, пока он не работал на многопроцессорном компьютере, обычно в производстве. Некоторые факторы, которые вызывают проблемы с видимостью, такие как локальные кэши ЦП и переупорядочение команд, - это то, что вы ожидаете от любой многопроцессорной машины. Однако устранение явно ненужных инструкций может произойти для любой машины. Ничто не заставляет JVM когда-либо заставлять читателя видеть актуальное значение переменной, вы зависите от разработчиков JVM. Поэтому мне кажется, что этот код не подходит для любой архитектуры ЦП.