Паттерн Singleton гарантирует, что для класса в приложении будет создан только один экземпляр и везде будет использоваться только он.
При попытке получить экземпялр класса, если это первое обращение, то будет либо создан экземпляр и возвращена ссылка на него если используется ленивая инициализация, либо будет возвращен указатель на объект который был создан при загрузке приложения если это не ленивая инициализация.
При последующих обращениях всегда будет возвращаться ссылка на уже существующий объект и как следствие во всех точках программы будет использоваться один единственный экземпляр класса.
Мой любимы. Пусть и скучный ;)
В Spring
@Component
public class BlaBlaSingleton {
// ...
}
В ASP.NET Core
//...
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<BlaBlaSingleton>();
//...
Не просто Singleton-ы, еще и DI подставит куда нужно. Just use it.
Ленивая, потоко-небезопасная реализация.
class BlaBlaSingleton {
private static BlaBlaSingleton instance = null;
private BlaBlaSingleton() {}
public static BlaBlaSingleton getInstance() {
if (instance == null) {
instance = new BlaBlaSingleton();
}
return instance;
}
}
Ленивая, потоко-безопасная реализация.
Но такая реализация требует захвата блокировки при каждом обращении к функции getInstance
class BlaBlaSingleton {
private static BlaBlaSingleton instance = null;
private BlaBlaSingleton() {}
public static synchronized BlaBlaSingleton getInstance() {
if (instance == null) {
instance = new BlaBlaSingleton();
}
return instance;
}
}
Ленивая, потоко-небезопасная реализация.
class BlaBlaSingleton
{
private static BlaBlaSingleton instance;
private BlaBlaSingleton() {}
public static BlaBlaSingleton getInstance()
{
if (instance == null)
instance = new BlaBlaSingleton();
return instance;
}
}
Ленивая, потоко-безопасная реализация.
Но такая реализация требует захвата блокировки при каждом обращении к свойству Instance
public sealed class BlaBlaSingleton
{
private BlaBlaSingleton() {}
private static BlaBlaSingleton instance = null;
private static readonly object lockObj = new object();
public static BlaBlaSingleton Instance
{
get
{
lock (lockObj)
{
if (instance == null)
instance = new BlaBlaSingleton();
return instance;
}
}
}
}
Ленивая, потоко-безопасная реализация.
public sealed class BlaBlaSingleton
{
private static readonly Lazy<BlaBlaSingleton> instanceHolder =
new Lazy<BlaBlaSingleton>(() => new BlaBlaSingleton());
private BlaBlaSingleton() {}
public static BlaBlaSingleton Instance
{
get { return instanceHolder.Value; }
}
}
Немного экзотический способ с использованием конструктора типа и гарантиями .net на ленивость и потоко-безопасность
public class BlaBlaSingleton<T> where T : class
{
protected BlaBlaSingleton() { }
private sealed class BlaBlaSingletonCreator<S> where S : class
{
private static readonly S instance = (S) typeof(S).GetConstructor(
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new Type[0],
new ParameterModifier[0]).Invoke(null);
public static S CreatorInstance
{
get { return instance; }
}
}
public static T Instance
{
get { return BlaBlaSingletonCreator<T>.CreatorInstance; }
}
}
// Usage
public class TestClass : BlaBlaSingleton<TestClass>
{
private TestClass() { }
public string MyFunction()
{
return "MyFunction";
}
}
Этот паттерн предназначен для снижения накладных расходов на синхронизацию.
Захват блокировки - это дорогостоящая операция. Если же захват блокировки оправдан не всегда то стоит ускорть этот процесс. Например для того, чтобы создать экземпляр singleton-а нам нужен эксклюзивный доступ и мы вынуждены захватывать блокировку даже когда экземпляр уже давно создан и все что нужно сделать - это просто вернуть ссылку.
Перед захватом блокировки мы сделаем проверку через if и если ссылка на инстанс уже доступна то просто вернем её не пытаясь войти в критическую секцию.
Если ссылки нет то мы пытаемся получить блокировку чтобы создать инстанс.
Когда, наконец, блокировка получина то мы должны опять проверить не создан ли уже инстанс. И если инстанс уже существует то просто вернуть его без создания нового, иначе создать новый и вернуть ссылку.
Вторая проверка необходима так как через первый if могло пройти два потока и один захватил блокировку и создал инстанс, а второй встал в ожидание на захват блокировки. Таким образом, если второй поток позже создас инстанс после получения блокировки то это будет второй инстанс singleton-а.
Итого:
Мы получаем и потокобезопасную и ленивую реализацию.
class BlaBlaSingleton {
private static BlaBlaSingleton instance = null;
private BlaBlaSingleton() {}
public static BlaBlaSingleton getInstance() {
if (instance == null) {
synchronized(this) {
if (instance == null) {
instance = new BlaBlaSingleton();
}
}
}
return instance;
}
}
и тут все как положено: проверка -> захват блокировки -> проверка -> создание объекта
public sealed class BlaBlaSingleton {
static BlaBlaSingleton instance = null;
static readonly object syncObj = new object();
BlaBlaSingleton() {}
public static BlaBlaSingleton Instance {
get {
if (instance == null) {
lock (syncObj) {
if (instance == null) {
tempInstance = new BlaBlaSingleton();
instance = tempInstance;
}
}
}
return instance;
}
}
}
Рассмотрим на примере Java.
Иногда, авторы советуют использовать volatile
при объявлении переменной для хранения ссылки на объект singleton-a. Примерно вот так
class BlaBlaSingleton {
private static volatile BlaBlaSingleton instance = null;
// ...
}
Аргумент здесь такой: volatile
переменная обеспечит нам happens-before и после того как первый поток запишет в нее ссылку на объект то память будет синхронизирована между потоками и они (другие потоки) увидят ее (ссылку на singleton) и не будут создавать свои инстансы.
если же посмотреть в документацию
We've already seen two actions that create happens-before relationships.
из документации следует что порядок happens-before
обеспечивается для:
то получается, что наличие synchronized
блока обеспечивает нужные нам свойства. И стоить еще помнить что при happens-before
между потоками будут синхронизированы все переменные.
Аналогично в C# / .NET предоставляются гарантии на Monitor.Enter()
и Monitor.Exit()
и видимость изменений между потоками.
Мы можем использолвать гарантий VM на инициализацию статических полей
потокобезопасная, без линивой инициализации и с возможностью обработать ошибки при инициализации инстанса BlaBlaSingleton
public class BlaBlaSingleton {
private static BlaBlaSingleton instance;
static {
instance = new BlaBlaSingleton();
}
private BlaBlaSingleton () {}
public static BlaBlaSingleton getInstance() {
return instance;
}
}
потокобезопасная и с ленивой инициализацией, но без возможности обработать ошибки при инициализации инстанса BlaBlaSingleton
public class BlaBlaSingleton {
private BlaBlaSingleton() {}
public static class BlaBlaSingletonHolder {
public static final BlaBlaSingleton INSTANCE = new BlaBlaSingleton();
}
public static BlaBlaSingleton getInstance() {
return BlaBlaSingletonHolder.INSTANCE;
}
}
В такой реализации возможны проблемы. Если в процессе инициализации инстанса BlaBlaSingleton произойдет ошибка
то мы получим либо ExceptionInInitializerError
либо NoClassDefFoundError
.
потокобезопасная и с ленивой инициализацией, но без возможности обработать ошибки при инициализации инстанса BlaBlaSingleton
public class BlaBlaSingleton
{
protected BlaBlaSingleton() { }
private sealed class BlaBlaSingletonCreator
{
public static readonly BlaBlaSingleton Instance = new BlaBlaSingleton();
}
public static BlaBlaSingleton Instance
{
get { return BlaBlaSingletonCreator.Instance; }
}
}
Очень похожие подходы, гарантии предоставляемые VM, реализации и т.п.
В C# немного больше подходов сделать singleton - это new Lazy<BlaBlaSingleton>
и конструктора типа.