Шаблон 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'.
Рассмотрим этот сценарий при использовании двойной проверки блокировки
- Поток A приходит первым, получает блокировку и переходит к инициализации объекта.
- Согласно модели памяти Java (JMM), память выделяется для переменной и публикуется до инициализации объекта Java.
- Входит поток B, и из-за двойной проверки блокировки и инициализации переменной он не получает блокировку.
- Это не гарантирует, что объект инициализирован, и даже если это так, кеш на процессор может не обновляться. Ссылка на кэш-последовательность
Теперь перейдем к ключевому слову volatile.
Изменчивые переменные всегда записываются в основную память. Отсюда и отсутствие кеш-памяти.