Full Stack Blog – C# vs Java. Singleton

05 March 2023

C# vs Java. Singleton

Немного теории

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

Характеристики

  • Потокобезопасность. Singleton может быть потокобезопасным или же не быть таковым.
  • Ленивость. Инициализация или создание экземпляра может выполняться в момент старта приложения или же лениво, в момент первого обращения к нему.

Как работает

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

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

Самый простой способ создать singleton

Мой любимы. Пусть и скучный ;)

В Spring

@Component
public class BlaBlaSingleton {
    // ...
}

The singleton scope is the default scope in Spring.

В ASP.NET Core

//...
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddSingleton<BlaBlaSingleton>();
//...

Не просто Singleton-ы, еще и DI подставит куда нужно. Just use it.

Java и Singleton-ы

Ленивая, потоко-небезопасная реализация.

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;
    }
}

C# и Singleton-ы

Ленивая, потоко-небезопасная реализация.

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";
    }
}

Double Checked Locking

Этот паттерн предназначен для снижения накладных расходов на синхронизацию.

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

Идея

Перед захватом блокировки мы сделаем проверку через if и если ссылка на инстанс уже доступна то просто вернем её не пытаясь войти в критическую секцию.

Если ссылки нет то мы пытаемся получить блокировку чтобы создать инстанс.

Когда, наконец, блокировка получина то мы должны опять проверить не создан ли уже инстанс. И если инстанс уже существует то просто вернуть его без создания нового, иначе создать новый и вернуть ссылку.

Вторая проверка необходима так как через первый if могло пройти два потока и один захватил блокировку и создал инстанс, а второй встал в ожидание на захват блокировки. Таким образом, если второй поток позже создас инстанс после получения блокировки то это будет второй инстанс singleton-а.

Итого:

  • первая проверка обеспечивает скорость работы после создание экземпляра
  • вторая проверка - это страховка от повторного создания объекта в многопоточном варианте

Мы получаем и потокобезопасную и ленивую реализацию.

Пример на Java

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;
    }
}

и тут все как положено: проверка -> захват блокировки -> проверка -> создание объекта

Пример на C#

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;
        }
    }
}

happens-before relationships

Рассмотрим на примере 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.

The synchronized and volatile constructs, as well as the Thread.start() and Thread.join() methods, can form happens-before relationships.

17.4.5. Happens-before Order

из документации следует что порядок happens-before обеспечивается для:

  • volatile переменные
  • synchronized, все локи
  • вход и выход из потока

то получается, что наличие synchronized блока обеспечивает нужные нам свойства. И стоить еще помнить что при happens-before между потоками будут синхронизированы все переменные.

Аналогично в C# / .NET предоставляются гарантии на Monitor.Enter() и Monitor.Exit() и видимость изменений между потоками.

Nested Class Holder

Мы можем использолвать гарантий VM на инициализацию статических полей

Java

потокобезопасная, без линивой инициализации и с возможностью обработать ошибки при инициализации инстанса 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.

C#

потокобезопасная и с ленивой инициализацией, но без возможности обработать ошибки при инициализации инстанса 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; }
    }
}

Conclusion

Очень похожие подходы, гарантии предоставляемые VM, реализации и т.п.

В C# немного больше подходов сделать singleton - это new Lazy<BlaBlaSingleton> и конструктора типа.