нам нужен volatile при реализации singleton с использованием блокировки с двойной проверкой
Предположим, мы используем блокировку с двойной проверкой для реализации шаблона singleton:
private static Singleton instance;
private static Object lock = new Object();
public static Singleton getInstance() {
if(instance == null) {
synchronized (lock) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
Нужно ли нам устанавливать переменную instance как volatile? Слышу поговорку, что это нужно для отключения переупорядочивания:
При создании объекта может произойти переупорядочение:
address=alloc
instance=someAddress
init(someAddress)
Они говорят, что если последние два шага переупорядочены, нам понадобится изменчивый экземпляр, чтобы отключить переупорядочение, иначе другие потоки могут получить объект, который не полностью инициализирован.
Однако, поскольку мы находимся в синхронизированном блоке кода, действительно ли нам нужна volatile? Или вообще, могу ли я сказать, что синхронизированный блок может гарантировать, что общая переменная прозрачна для других потоков и нет переупорядочения, даже если это не изменяемая переменная?
1 ответ
Прежде чем я перейду к этому объяснению, вам нужно понять одну оптимизацию, которую делают компиляторы (мое объяснение очень упрощено). Предположим, что где-то в вашем коде есть такая последовательность:
int x = a;
int y = a;
Для компилятора вполне допустимо переупорядочить их в:
// reverse the order
int y = a;
int x = a;
Ни один. Никто writes
к a
здесь всего два reads
из a
, поэтому такой вид переупорядочивания разрешен.
Чуть более сложный пример:
// someone, somehow sets this
int a;
public int test() {
int x = a;
if(x == 4) {
int y = a;
return y;
}
int z = a;
return z;
}
Компилятор может взглянуть на этот код и заметить, что если он введен if(x == 4) { ... }
, этот: int z = a;
никогда не бывает. Но, в то же время, вы можете думать об этом немного иначе: если этоif statement
введен, нам все равно, если int z = a;
выполняется или нет, это не меняет того факта, что:
int y = a;
return y;
все равно произойдет. Таким образом, давайте сделаем этоint z = a;
быть нетерпеливым:
public int test() {
int x = a;
int z = a; // < --- this jumped in here
if(x == 4) {
int y = a;
return y;
}
return z;
}
И теперь компилятор может дополнительно переупорядочить:
// < --- these two have switched places
int z = a;
int x = a;
if(x == 4) { ... }
Вооружившись этими знаниями, мы можем попытаться понять, что происходит.
Посмотрим на ваш пример:
private static Singleton instance; // non-volatile
public static Singleton getInstance() {
if (instance == null) { // < --- read (1)
synchronized (lock) {
if (instance == null) { // < --- read (2)
instance = new Singleton(); // < --- write
}
}
}
return instance; // < --- read (3)
}
Есть 3 чтения instance
(также называется load
) и одиночный write
к нему (также называемый store
). Как бы странно это ни звучало, но еслиread (1)
видел instance
это не ноль (это означает, что if (instance == null) { ... }
не введен), это не значит, что read (3)
вернет ненулевой экземпляр, он отлично подходит для read (3)
все еще вернуться null
. Это должно растопить ваш мозг (я делал это несколько раз). К счастью, есть способ доказать это.
Компилятор может добавить в ваш код такую небольшую оптимизацию:
public static Singleton getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new Singleton();
// < --- we added this
return instance;
}
}
}
return instance;
}
Он вставил return instance
, семантически это никак не меняет логику кода.
Затем есть определенная оптимизация, которую делают компиляторы, которая нам здесь поможет. Я не буду вдаваться в подробности, но он вводит некоторые локальные поля (преимущество в этой ссылке) для выполнения всех операций чтения и записи (сохранения и загрузки).
public static Singleton getInstance() {
Singleton local1 = instance; // < --- read (1)
if (local1 == null) {
synchronized (lock) {
Singleton local2 = instance; // < --- read (2)
if (local2 == null) {
Singleton local3 = new Singleton();
instance = local3; // < --- write (1)
return local3;
}
}
}
Singleton local4 = instance; // < --- read (3)
return local4;
}
Теперь компилятор может посмотреть на это и увидеть, что: если if (local2 == null) { ... }
введен, Singleton local4 = instance;
никогда не происходит (или, как сказано в примере, с которого я начал этот ответ: на самом деле не имеет значения, если Singleton local4 = instance;
бывает у всех). Но чтобы войтиif (local2 == null) {...}
, нам нужно ввести это if (local1 == null) { ... }
первый. А теперь рассмотрим это в целом:
if (local1 == null) { ... } NOT ENTERED => NEED to do : Singleton local4 = instance
if (local1 == null) { ... } ENTERED && if (local2 == null) { ... } NOT ENTERED
=> MUST DO : Singleton local4 = instance.
if (local1 == null) { ... } ENTERED && if (local2 == null) { ... } ENTERED
=> CAN DO : Singleton local4 = instance. (remember it does not matter if I do it or not)
Как видите, во всех случаях это не вредно: Singleton local4 = instance
перед любыми проверками if.
После всего этого безумия ваш код может стать:
public static Singleton getInstance() {
Singleton local4 = instance; // < --- read (3)
Singleton local1 = instance; // < --- read (1)
if (local1 == null) {
synchronized (lock) {
Singleton local2 = instance; // < --- read (2)
if (local2 == null) {
Singleton local3 = new Singleton();
instance = local3; // < --- write (1)
return local3;
}
}
}
return local4;
}
Есть два независимых прочтения instance
Вот:
Singleton local4 = instance; // < --- read (3)
Singleton local1 = instance; // < --- read (1)
if(local1 == null) {
....
}
return local4;
Ты читаешь instance
в local4
(предположим null
), то вы читаете instance
в local1
(предположим, что какой-то поток уже изменил это на ненулевое значение) и... ваш getInstance
вернет null
, а не Singleton
. qed
Вывод: эти оптимизации возможны только приprivate static Singleton instance;
является non-volatile
, иначе большая часть оптимизации запрещена, и ничего подобного было бы невозможно. Итак, да, используяvolatile
ОБЯЗАТЕЛЬНО для правильной работы этого шаблона.