Шаблон Singleton в многопоточной среде

Во время моего интервью интервьюер начал свой вопрос с одноэлементной картины. Я написал ниже. Затем он спросил: "Не следует ли мы проверить" Недействительность "внутри метода getInstance?

Я ответил: " НЕ), поскольку член является статическим и одновременно инициализируется. Но, похоже, он не удовлетворился моим ответом. Я правильно или нет?

class Single {

        private final static Single sing = new Single();       
        private Single() {
        }        
        public static Single getInstance() {
            return sing;
        }
    }

Теперь, следующий вопрос, он просит написать одноэлементный класс для многопоточной среды. Затем я написал двойной одноэлементный класс проверки.

  class MultithreadedSingle {        
        private static MultithreadedSingle single;       
        private MultithreadedSingle() {
        }        
        public static MultithreadedSingle getInstance() {
            if(single==null){
                    synchronized(MultithreadedSingle.class){
                      if(single==null){
                            single= new MultithreadedSingle(); 
                              }      
                      }
                   }
             return single;
        }
    }

Затем у него возникло возражение против использования synchronized и двойной проверки и сказал, что это бесполезно. Почему вы дважды проверяете и почему используете синхронизацию? Я пытался убедить его в нескольких сценариях. Но он этого не сделал.

Позже, дома, я попробовал под кодом, где я использую простой одноэлементный класс с несколькими потоками.

public class Test {

    public static void main(String ar[]) {
        Test1 t = new Test1();
        Test1 t2 = new Test1();
        Test1 t3 = new Test1();
        Thread tt = new Thread(t);
        Thread tt2 = new Thread(t2);
        Thread tt3 = new Thread(t3);
        Thread tt4 = new Thread(t);
        Thread tt5 = new Thread(t);
        tt.start();
        tt2.start();
        tt3.start();
        tt4.start();
        tt5.start();

    }
}

final class Test1 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " : " + Single.getInstance().hashCode());
        }
    }

}
     class Single {

        private final static Single sing = new Single();       
        private Single() {
        }        
        public static Single getInstance() {
            return sing;
        }
    }

Ниже представлен результат:

Thread-0 : 1153093538
Thread-0 : 1153093538
Thread-0 : 1153093538
Thread-0 : 1153093538
Thread-0 : 1153093538
Thread-4 : 1153093538
Thread-1 : 1153093538
Thread-2 : 1153093538
Thread-3 : 1153093538
Thread-3 : 1153093538
Thread-3 : 1153093538
Thread-3 : 1153093538
Thread-3 : 1153093538
Thread-2 : 1153093538
Thread-2 : 1153093538
Thread-2 : 1153093538
Thread-2 : 1153093538
Thread-1 : 1153093538
Thread-1 : 1153093538
Thread-1 : 1153093538
Thread-1 : 1153093538
Thread-4 : 1153093538
Thread-4 : 1153093538
Thread-4 : 1153093538
Thread-4 : 1153093538

Итак, вопрос в том, нужно ли использовать метод synchronize или/и двойной проверки в многопоточной среде? Кажется, что мой первый код (без добавления дополнительной строки кода) был ответом для обоих вопросов. Любые поправки и доля знаний будут оценены.

Ответ 1

Ваш первый пример абсолютно правильный и обычно является предпочтительной "идиомой" для одиночек. Другой - сделать одноэлементное перечисление:

public enum Single {
    INSTANCE;

    ...
}

Два подхода похожи друг на друга, если класс не является Serializable, и в этом случае подход enum намного легче получить правильно - но если класс не является Serializable, я фактически предпочитаю ваш подход перечислением один, как стилистический дело. Следите за тем, чтобы "случайно" стало Serializable из-за реализации интерфейса или расширения класса, который сам является Serializable.

Вы также правы в отношении второй проверки недействительности в примере с двойной проверкой блокировки. Однако поле sing должно быть volatile, чтобы это работало на Java; в противном случае нет формального края "случится раньше" между одним потоком, записываемым в sing, и другим потоком, читающим его. Это может привести к тому, что второй поток видит null, даже если первый поток назначен этой переменной, или если экземпляр sing имеет состояние, это может даже привести к тому, что второй поток видит только некоторое из этого состояния (видя частично -строенный объект).

Ответ 2

1) КласС# 1 хорош для многопоточной среды

2) Класс №2 - это синглтон с ленивой инициализацией и двойной проверкой блокировки, это известный шаблон, и ему нужно использовать синхронизацию. Но ваша реализация нарушена, ей нужно volatile на поле. Вы можете узнать, почему в этой статье http://www.javaworld.com/article/2074979/java-concurrency/double-checked-locking--clever--but-broken.html

3) Синглтон с одним методом не должен использовать ленивый шаблон, потому что его класс будет загружен и инициализирован только при первом использовании.

Ответ 3

Ваш первый ответ, кажется, хорош для меня, так как нет никаких шансов на состояние гонки.

Что касается обмена знаниями, лучший подход для реализации синглтона в Java - это использование Enum. Создайте перечисление ровно с одним экземпляром, и вот оно. Что касается примера кода -

public enum MyEnum {
    INSTANCE;

    // your other methods
}

Из хорошей книги Эффективная Ява -

[....] Этот подход функционально эквивалентен подходу публичного поля, за исключением того, что он намного более лаконичен, предоставляет механизм сериализации бесплатно и обеспечивает железную гарантию от множественных реализаций, даже в условиях сложной сериализации или рефлексии. Атаки. [...] Одноэлементный тип перечисления - лучший способ реализовать синглтон.

Ответ 4

в соответствии с Double-checked_locking, вероятно, это лучший способ

class Foo {
    private volatile Helper helper;
    public Helper getHelper() {
        Helper result = helper;
        if (result == null) {
            synchronized(this) {
                result = helper;
                if (result == null) {
                    helper = result = new Helper();
                }
            }
        }
        return result;
    }
}

или используя Инициализация по требованию владельца идиомы

public class Something {
    private Something() {}

    private static class LazyHolder {
        private static final Something INSTANCE = new Something();
    }

    public static Something getInstance() {
        return LazyHolder.INSTANCE;
    }
}

Ответ 5

В случае № 2 добавьте ключевое слово 'volatile' в статическое поле 'single'.

Рассмотрим этот сценарий при использовании двойной проверки блокировки

  1. Поток A приходит первым, получает блокировку и переходит к инициализации объекта.
  2. Согласно модели памяти Java (JMM), память выделяется для переменной и публикуется до инициализации объекта Java.
  3. Входит поток B, и из-за двойной проверки блокировки и инициализации переменной он не получает блокировку.
  4. Это не гарантирует, что объект инициализирован, и даже если это так, кеш на процессор может не обновляться. Ссылка на кэш-последовательность

Теперь перейдем к ключевому слову volatile.

Изменчивые переменные всегда записываются в основную память. Отсюда и отсутствие кеш-памяти.