DEV Community

faangmaster
faangmaster

Posted on

Thread Safe Java Singleton

Задача. Что не так с этим кодом?

    public class Singleton {
        private static Singleton instance;

        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
       // private constructor and other methods ...
    }
Enter fullscreen mode Exit fullscreen mode

Решение. Этот код пытается создать Singleton в Java. Т.е. Java объект, который будет существовать только в одном экземпляре в программе. Причем делает это ленивым образом (lazy initialization). Он не создает объект с самого начала, а создает его лишь тогда, когда кто-то впервые вызовет метод getInstance().

Более того, он пытается сделать этот код потокобезопасным. Чтобы он работал правильно в многопоточной среде.

Подход использованный в этом коде называется Double-Checked Locking. Т.к. в коде дважды присутствует проверка instance == null. Это делается для того, чтобы избежать потенциально не нужного получения лока (например когда делаем весь метод getInstance() synchonized).

Например, два потока одновременно вызывают метод getInstance(). Пусть оба потока проверили, что instance == null. И для обоих потоков он null. Тогда какой-то из потоков получит лок на классе Singleton.class. А второй будет ждать. Первый поток выполнит код в synchronized блоке и инициализирует объект instance и отпустит лок. После второй поток получит лок, но когда второй раз выполнит проверку instance == null, то она уже вернет не null и повторное создание объекта уже не требуется.

Теперь другая ситуация. Снова два потока вызвали метод getInstance() практически одновременно. Пусть первый поток был чуть быстрее и успел выполнить первую проверку instance == null и выполнил весь synchronized блок. А второй поток только начал проверять intance == null в первом if. Для него intance будет уже не null. Тогда он пропустит synchonized блок и выполнит return instance. При этом он не будет получать никаких локов или их ждать.

Т.е. все как будто работает как мы и хотели. Но из-за того, что в описанном втором случае, создание и запись в переменную делается в synchronized блоке, а чтение не в synchonized блоке во втором потоке, то Java не может гарантировать, что объект, который прочитает второй поток будет полностью инициализированным и корректно опубликованным (смотри Java Memory Model, happens before и Safe Publication*).* Для этого надо или и запись и чтение помещать в synchronized блок или переменную, которую мы читаем сделать volatile. Volatile может решить проблему с visibility (но не с атомарностью) между потоками из-за возможных неконсистентных состояний кешей ядер процессора и проблемы re-ordering. До Java 1.5 Double-Checked Locking в Java не работал вообще. Начиная с Java 1.5 делая переменную volatile можно заставить такой код работать.

Т.е. код можно починить добавив volatile в объявление переменной instance:

    public class Singleton {
        private static volatile Singleton instance;

        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
       // private constructor and other methods ...
    }
Enter fullscreen mode Exit fullscreen mode

Как еще можно создать Singleton в Java?

Если среда не многопоточная, то:

    public class Singleton {
        private static Singleton instance;

        public static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
       // private constructor and other methods ...
    }
Enter fullscreen mode Exit fullscreen mode

Если многопоточная, то:

Полностью сделать метод getInstance() synchronized (но, тогда всегда нужно будет получать лок, что будет сказываться на производительности программы):

    public class Singleton {
        private static Singleton instance;

        public synchronized static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
       // private constructor and other methods ...
    }
Enter fullscreen mode Exit fullscreen mode

Инициализировать объект сразу (не использовать lazy-initialization), но тогда объект будет создаваться всегда, даже если его никто не запросил:

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

        public static Singleton getInstance() {
            return instance;
        }
       // private constructor and other methods ...
    }
Enter fullscreen mode Exit fullscreen mode

Для достижения lazy-initialization можно использовать внутренний класс:

    public class SingletonFactory {
        private static class SignletonHolder {
          public static Singleton instance = new Singleton();
        }

        public static Singleton getInstance() {
            return SignletonHolder.instance;
        }
    }
Enter fullscreen mode Exit fullscreen mode

Еще вариант через Enum:

    public enum EnumSingleton {

        INSTANCE("Initial class info"); 

        private String info;

        private EnumSingleton(String info) {
            this.info = info;
        }

        public EnumSingleton getInstance() {
            return INSTANCE;
        }

        // getters and setters
    }

    Использование:

    EnumSingleton.INSTANCE.getInstance()
Enter fullscreen mode Exit fullscreen mode

Top comments (0)