Java Singleton с внутренним классом - что гарантирует безопасность потоков?

Один общий (1, 2) способ реализации singleton использует внутренний класс с статический член:

public class Singleton  {    
    private static class SingletonHolder {    
        public static final Singleton instance = new Singleton();
    }    

    public static Singleton getInstance() {    
        return SingletonHolder.instance;    
    }

    private Singleton() {
        //...
    }
}

Эта реализация называется лениво инициализированной и потокобезопасной. Но что именно гарантирует безопасность потока? JLS 17, который имеет дело с Threads and Locks, не говорит о том, что статические поля имеют какие-либо отношения до и после. Как я могу быть уверен, что инициализация произойдет только один раз и что все потоки будут видеть один и тот же экземпляр?

Ответ 1

Мы должны сначала понять две точки:

Статическая инициализация происходит только один раз при загрузке класса

Поля, которые имеют статический модификатор в объявлении, называются статическими полями или переменными класса. Они связаны с классом, а не с каким-либо объектом. Каждый экземпляр класса использует переменную класса, которая находится в одном фиксированном месте в памяти

....

Инициализация класса состоит в выполнении его статических инициализаторов и инициализаторов для статических полей (переменных класса), объявленных в классе

Это означает, что статические инициализаторы выполняются только один раз при инициализации класса объекта (фактический объект класса, а не экземпляр класса).

Поскольку язык программирования Java многопоточен, инициализация класса или интерфейса требует тщательной синхронизации, поскольку другой поток может пытаться инициализировать один и тот же класс или интерфейс в одно и то же время.

Для каждого класса или интерфейса C существует уникальный замок инициализации LC. Отображение от C до LC остается на усмотрение реализации виртуальной машины Java.

Теперь, простыми словами, когда два потока пытаются инициализировать instance, первый поток, который приобретает LC, является тем, который фактически инициализирует instnace, и поскольку он делает это статически, java дает обещание, что это происходит только один раз.

Подробнее о блокировке инициализации читайте JSL 17

Ответ 2

Хорошо описано в Java Concurrency на практике:

В ленивой инициализации класса idiom используется класс, единственной целью которого является инициализация ресурса. JVM откладывает инициализацию класса ResourceHolder до тех пор, пока он не будет на самом деле используется [JLS 12.4.1], и потому что ресурс инициализирован со статическим инициализатором, дополнительной синхронизации не требуется. Первый вызов getresource по любому потоку вызывает ResourceHolder для загружаться и инициализироваться, при этом инициализация Ресурс происходит через статический инициализатор.

Статическая инициализация

Статические инициализаторы запускаются JVM во время инициализации класса, после загрузки класса, но до того, как класс используется любым потоком. Поскольку JVM получает блокировку во время инициализации [JLS 12.4.2] и эта блокировка приобретается каждым потоком хотя бы один раз, чтобы гарантировать, что класс загружен, записи в память во время статической инициализации автоматически отображаются для всех потоков. Таким образом, статически инициализировано объекты не требуют явной синхронизации либо во время строительства или при ссылке.