1. Одноэлементный класс в голодном стиле
}
частный статический экземпляр Singleton = новый Singleton();
частный статический синглтон getInstance(){
вернуть экземпляр;
}
}
Возможности: предварительное создание экземпляров голодного стиля, в ленивом стиле нет проблем с многопоточностью, но независимо от того, вызываем ли мы getInstance() или нет, в памяти будет экземпляр.
2. Внутренний класс-одиночка
}
частный класс SingletonHoledr(){
частный статический экземпляр Singleton = новый Singleton();
}
частный статический синглтон getInstance(){
вернуть SingletonHoledr.instance;
}
}
Особенности: Во внутреннем классе реализована отложенная загрузка. Только когда мы вызываем getInstance(), в памяти создается уникальный экземпляр. Это также решает проблему многопоточности в ленивом стиле. Решение состоит в использовании характеристики Classloader. .
3. Ленивый класс Singleton
}
частный статический экземпляр Singleton;
публичный статический синглтон getInstance(){
если (экземпляр == ноль) {
возвращаемый экземпляр = новый Singleton();
}еще{
вернуть экземпляр;
}
}
}
Особенности: В ленивом стиле есть потоки A и B. Когда поток A переходит к строке 8, он переходит к потоку B. Когда B также переходит к строке 8, экземпляры обоих потоков пусты, поэтому он генерирует два примера. . Решение состоит в синхронизации:
Синхронизация возможна, но не эффективна:
}
частный статический экземпляр Singleton;
публичный статический синхронизированный синглтон getInstance(){
если (экземпляр == ноль) {
возвращаемый экземпляр = новый Singleton();
}еще{
вернуть экземпляр;
}
}
}
При написании такой программы не будет никакой ошибки, потому что весь getInstance представляет собой целую «критическую секцию», но эффективность очень низкая, потому что наша цель на самом деле только в том, чтобы заблокировать при первой инициализации экземпляра, а затем получить экземпляр. При использовании экземпляра вообще нет необходимости в синхронизации потоков.
Поэтому умные люди придумали следующий подход:
Метод записи блокировки двойной проверки:
public static Singleton getSingle(){ //Внешние объекты можно получить с помощью этого метода
если (одиночный == ноль) {
синхронизированный (Singleton.class) { //Это гарантирует, что только один объект может получить доступ к этому синхронизированному блоку одновременно
если (одиночный == ноль) {
сингл = новый синглтон ();
}
}
}
return Single; //Вернем созданный объект;
}
}
Идея очень проста, то есть нам нужно синхронизировать (синхронизировать) только ту часть кода, которая инициализирует экземпляр, чтобы код был одновременно корректным и эффективным.
Это так называемый механизм «двойной проверки блокировки» (как следует из названия).
К сожалению, такой способ написания неверен на многих платформах и оптимизирующих компиляторах.
Причина в том, что поведение строки кода instance = new Singleton() на разных компиляторах непредсказуемо. Оптимизирующий компилятор может легально реализовать instance = new Singleton() следующим образом:
1. экземпляр = выделить память для нового объекта
2. Вызовите конструктор Singleton для инициализации переменных-членов экземпляра.
Теперь представьте, что потоки A и B вызывают getInstance. Поток A входит первым и выбрасывается из процессора при выполнении шага 1. Затем входит поток B, и B видит, что экземпляр больше не равен нулю (память была выделена), поэтому он начинает уверенно использовать экземпляр, но это неправильно, потому что в этот момент переменные-члены экземпляра все еще являются значениями по умолчанию. значение, A еще не успел выполнить шаг 2 для завершения инициализации экземпляра.
Конечно, компилятор может реализовать это и так:
1. temp = выделить память
2. Вызовите конструктор temp
3. экземпляр = температура
Если компилятор ведет себя так, у нас вроде бы нет проблем, но на самом деле все не так просто, потому что мы не можем знать, как тот или иной компилятор это делает, потому что эта проблема не определена в модели памяти Java.
Блокировка двойной проверки применима к базовым типам (таким как int). Очевидно, потому что базовый тип не вызывает конструктор.