-
- public class Singleton {
- private static Singleton instance;
-
- private Singleton {
- }
-
- public static Singleton getInstance() {
- if(instance == null) {
- synchronized(this) {
- if(instance == null) {
- instance = new Singleton();
- }
- }
- }
- return instance;
- }
- }
-
По логике все прекрасно, и создание объекта on-demand (ленивая инициализация), и не блокирующие чтение. Но, к сожалению, в некоторых случаях данный Singleton будет вести себя плохо. В чем же причина? Все дело в нюансах работы JVM с памятью, в многопоточной среде. Прежде чем описать проблему и приступить к решению, нужно вспомнить основные идиомы синхронизации при работе с памятью.
— Атомарность. Целостность доступа (чтения и записи) к переменной в памяти. Гарантия что при запросе переменной будет возвращено ее значение либо до изменения одним из потоков, либо после. Но никак не промежуточное состояние (набор битов старого значения переписанное частично другим потоком).
— Видимость. Видимость переменных измененных одним потоком в контексте другого.
— Упорядоченность. Последовательно выполнение команд скомпилированного кода.
Если блок кода или метод синхронизирован, то все изменения, сделанные в контексте одного потока, атомарны, видимы для остальных потоков (использующих туже блокировку) и выполняются последовательно в соответствие с порядком кода в программе. Если синхронизации нет — все вышесказанное не гарантировано. Где же теряется синхронизация в нашем примере? Самое очевидное это отсутствие упорядоченности в строке instance = new Singleton ();. Когда компилятор видит конструктор, который не выбрасывает исключений и не выполняется синхронизированно, он может свободно поменять порядок инициализации объекта и его запись в перменную instance. Даже если порядок записи не меняется компилятором, он может быть изменен процессором в мультипроцессорной среде или самой системой памяти.
Самое важное в данной ситуации не придумывать велосипед, т.к. решая одну проблему синхронизации можно напороться на другую, которая проявит себя в специфичной ситуации совершенно не к месту. Многие гуру бились над этой проблемой и выработали приемлемые решения. Приводить обзор всех вариантов я не буду, укажу лишь только то, что использую сам.
1. Класс-холдер.
-
- public class Singleton {
-
- private Singleton {
- }
-
- private static class SingletonHolder {
- private final static Singleton instance = new Singleton();
- }
-
- public static Singleton getInstance() {
- return SingletonHolder.instance;
- }
- }
-
Красивый способ, без явной синхронизации, инициализация on-demand. Но конструктор не должен выбрасывать эксепшенов, если есть уверенность что исключений не будет — используем этот вариант.
2. Доработанный double-checked locking пример.
-
- public class Singleton {
- private volatile static Singleton instance;
-
- private Singleton {
- }
-
- public static Singleton getInstance() {
- if(instance == null) {
- synchronized(this) {
- if(instance == null) {
- instance = new Singleton();
- }
- }
- }
- return instance;
- }
- }
-
Модификатор volatile гарантирует синхронизированное обращение к перменной instance. Но работает только на jdk 5 и выше.
Статья изложена, осознана, переведена с использованием источников:
Реализация Singleton в JAVA
Synchronization and the Java Memory Model
The «Double-Checked Locking is Broken» Declaration
______________________
Комментариев нет:
Отправить комментарий