Friday, August 6, 2010

Знакомьтесь, антипаттерн double-checked locking

На днях размышлял, как можно было бы ускорить следующую конструкцию:
Конструкция 1. Известная всем реализация ленивого синглтона.

public class Singleton
{
private Singleton(){}

private static Singleton INSTANCE;

public static synchronized Singleton getInstance()
{
if (INSTANCE == null)
{
INSTANCE = new Singleton();
//
// ... perform an INSTANCE initialization
}
return INSTANCE;
}
}

Основным недостатком является наличие synchronized, которое по-хорошему нужно
только при инициализации экземпляра объекта singleton, а не при каждом доступе к нему.
Таким образом, сразу же напрашивается мысль, а что если попробовать использовать критическую секцию только при инициализации объекта:
Конструкция 2. Первая попытка сделать короче время доступа к INSTANCE.

public class Singleton
{
private Singleton(){}

private static Singleton INSTANCE;

public static Singleton getInstance()
{
if (INSTANCE == null)
{
synchronized(Singleton.class)
{
INSTANCE = new Singleton();
//
// ... perform an INSTANCE initialization
}
}
return INSTANCE;
}
}

Теперь инициализация объекта будет происходить при первом доступе к объекту, но, к сожалению, данная конструкция не спасает от конкурентного доступа. Так как может получиться так, что конкурирующие процессы после проверки INSTANCE на null последовательно войдут в критическую секцию и проинициализируют Singleton несколько раз. Тут же придумал workaround: поставить проверку на null после входа в критическую секцию, сказано - сделано:
Конструкция 3. Вторая попытка или double-checked locking антиппатерн.
public class Singleton
{
private Singleton(){}

private static Singleton INSTANCE;

public static Singleton getInstance()
{
if (INSTANCE == null)
{
synchronized(Singleton.class)
{
if (INSTANCE == null)
{
INSTANCE = new Singleton();
//
// ... perform an INSTANCE initialization
}
}
}
return INSTANCE;
}
}

Полученная конструкция в теории должна работать, но так ли это? Довольный своим открытием, я решил посмотреть, что пишут эксперты о такой конструкции, есть ли какие-то другие, более эффективные варианты ускорения реализации паттерна singleton. Очень быстро обнаружил, что такая конструкция действительно используется и называется она double-checked locking (в некоторых источниках такую конструкцию называют даже антипаттерном [2]). Действительно она применяется для ускорения singleton, но имеет ряд неочевидных недостатков для некоторых языков.

Недостаток первый. В некоторых языках, в т.ч. и в Java значение переменной INSTANCE может быть присвоено при выполнении конструктора до его завершения. Это связано с выделением памяти, как только выделяется память, то INSTANCE получает соотв. значение (в Java это ссылка, в других языках это, возможно, указатель на некоторую выделенную для объекта область памяти). Таким образом, есть вероятность, что пока один поток будет выполняться в критической секции, другой, выполнив проверку INSTANCE на null, в критиескую секцию уже не пойдет, а сразу вернет неинициализированную (но не равную null) переменную INSTANCE.

Недостаток второй. Запись и чтение значений переменных в многопоточных программах на некоторых языках могут зависеть от реализации конкретной исполняющей среды/компилятора. Например, в JAVA используется кеширование переменных: из соображений эффективности каждый поток может хранить свою собственную приватную копию переменной. Эта копия может синхронизироватся с основной памятью в различные моменты, например, при входе в критическую секцию и при выходе из нее [3]. Как вариант, в Java можно использовать модификатор volatile для переменной INSTANCE, которое действительно гарантирует атомарный доступ к переменной. В таком случае переменная никогда не кешируется потоками и доступ к ней осуществляется, как если бы это происходило внутри критической секции (synchronize над этой переменной). Но по сути это приводит к тому же от, чего уходили - к наличию критической секции при доступе к переменной.

Таким образом, рекомендуется использовать конструкцию 1 [2], либо использовать факт о ленивой загрузке классов [1]: т.е. что сам класс Singleton будет загружен лишь при первом обращении к нему, соотв. будут проинициализированны все static-поля и секции:
Конструкция 4. Если быть проще...
public class Singleton
{
private static Singleton INSTANCE = new Singleton();

static
{
// further INSTANCE initialization
}

private Singleton()
{
// Singleton initialization
}

public static Singleton getInstance()
{
return INSTANCE;
}
}


Материалы.

[1] http://www.ibm.com/developerworks/java/library/j-dcl.html
[2] http://www.javamex.com/tutorials/double_checked_locking_fixing.shtml
[3] http://www.javamex.com/tutorials/synchronization_concurrency_synchronized1.shtml

2 comments:

  1. Можно ещё красивее через lazy initialization holder:

    public class SIngletonFactory {
    private static class SIngletonHolder {
    public static SIngleton resource = new SIngleton();
    }

    public static SIngleton init() {
    return SIngletonHolder.resource;
    }
    }

    ReplyDelete
  2. А можно нормально сделать через enum

    ReplyDelete