Однажды я написал статью: «Знаете, существует 8 способов написать шаблон синглтона? ", в котором упоминается метод реализации синглтонов — блокировок с двойной проверкой. Недавно я прочитал книги по параллелизму и обнаружил, что неправильное использование блокировок с двойной проверкой не совсем безопасно. Поделитесь им здесь.
Синглтон Обзор
Сначала давайте рассмотрим, как выглядит простейший одноэлементный шаблон?
/**
*单例模式一:懒汉式(线程安全)
*/
public class Singleton1 {
private static Singleton1 singleton1;
private Singleton1() {
}
public static Singleton1 getInstance() {
if (singleton1 == null) {
singleton1 = new Singleton1();
}
return singleton1;
}
}
Это ленивая одноэлементная реализация. Как мы все знаем, из-за отсутствия соответствующего механизма блокировки эта программа небезопасна для потоков. Самый быстрый способ добиться безопасности — добавить синхронизированный
/**
* 单例模式二:懒汉式(线程安全)
*/
public class Singleton2 {
private static Singleton2 singleton2;
private Singleton2() {
}
public static synchronized Singleton2 getInstance() {
if (singleton2 == null) {
singleton2 = new Singleton2();
}
return singleton2;
}
}
После использования synchronized можно гарантировать потокобезопасность, но synchronized блокирует все блоки кода, что приведет к большим потерям производительности, поэтому люди придумали «умный» трюк: блокировка с двойной проверкой (DCL) (double Checked Locking) Механизм реализует синглтон.
замок с двойной проверкой
Синглтон для реализации блокировки с двойной проверкой выглядит так:
/**
* 单例模式三:DCL(double checked locking)双重校验锁
*/
public class Singleton3 {
private static Singleton3 singleton3;
private Singleton3() {
}
public static Singleton3 getInstance() {
if (singleton3 == null) {
synchronized (Singleton3.class) {
if (singleton3 == null) {
singleton3 = new Singleton3();
}
}
}
return singleton3;
}
}
Как показано в приведенном выше коде, если первый экземпляр проверки не равен нулю, то следующие операции блокировки и инициализации выполнять не нужно. Следовательно, потери производительности, вызванные синхронизацией, могут быть значительно снижены. На первый взгляд, приведенный выше код, кажется, сочетает в себе лучшее из обоих миров:
- Когда несколько потоков пытаются одновременно создать объект, используется блокировка, чтобы гарантировать, что только один поток может создать объект.
- После того, как объект создан, выполнение getInstance() вернет созданный объект напрямую, без получения блокировки.
Программа выглядит идеально, но это неполная оптимизация.Когда поток выполняет 9-ю строку кода и читает, что экземпляр не нулевой (первое if), возможно, объект, на который ссылается экземпляр, не был инициализирован.
корень проблемы
Проблема в операторе, который создает объектsingleton3 = new Singleton3();
Выше создание объекта в java не является атомарной операцией и может быть разбито на три строки псевдокода:
//1:分配对象的内存空间
memory = allocate();
//2:初始化对象
ctorInstance(memory);
//3:设置instance指向刚分配的内存地址
instance = memory;
Между 2 и 3 в приведенных выше трех строках псевдокода может быть переупорядочен (в некоторых JIT-компиляторах), то есть компилятор или процессор меняет порядок выполнения кода для повышения производительности.Содержание этой части будет подробно объяснено позже. ., псевдокод после переупорядочения выглядит следующим образом:
//1:分配对象的内存空间
memory = allocate();
//3:设置instance指向刚分配的内存地址
instance = memory;
//2:初始化对象
ctorInstance(memory);
В однопоточной программе переупорядочивание не повлияет на конечный результат, но в случае параллелизма некоторые потоки могут обращаться к неинициализированным переменным.
Смоделируйте сценарий, в котором 2 потока создают синглтон, как показано в следующей таблице:
время | нить А | нить Б |
---|---|---|
t1 | A1: Выделить место в памяти объекта | |
t2 | A3: установите экземпляр так, чтобы он указывал на пространство памяти | |
t3 | B1: определить, пуст ли экземпляр | |
t4 | B2: поскольку экземпляр не равен нулю, поток B получит доступ к объекту, на который ссылается экземпляр. | |
t5 | A2: Инициализируйте объект | |
t6 | A4: Доступ к объектам, на которые ссылается экземпляр |
В этом порядке поток B получит неинициализированный объект, и потоку B не нужно будет получать блокировку от начала до конца!
изменение порядка инструкций
Ранее мы проанализировали, что причина проблемы кроется в «переупорядочивании инструкций», так что же такое «переупорядочивание инструкций» и почему оно влияет на результаты обработки программы при одновременном выполнении? Сначала давайте рассмотрим концепцию «последовательно непротиворечивой модели памяти».
Теоретическая модель последовательной согласованности памяти
Sequentially Consistent Memory Model — теоретическая эталонная модель, идеализированная учеными-компьютерщиками, которая дает программистам надежные гарантии видимости памяти. Модель последовательной согласованной памяти имеет две основные характеристики:
- Все операции в потоке должны выполняться в порядке программы.
- Все потоки (независимо от синхронизации программ) видят только единый порядок выполнения операций. В последовательно согласованной модели памяти каждая операция должна выполняться атомарно и быть видимой для всех потоков одновременно.
Фактическая модель JMM
Однако модель последовательной согласованности является лишь идеализированной моделью, а в реальной реализации JMM с целью максимизации эффективности работы программы имеет следующие отличия от идеальной модели памяти последовательной согласованности:
В модели последовательной согласованности все операции выполняются последовательно в точном порядке программы. Не гарантируется, что однопоточные операции будут выполняться в порядке программы в JMM (т.指令重排序
).
Модель последовательной согласованности гарантирует, что все потоки могут видеть только один и тот же порядок выполнения операций, в то время как JMM не гарантирует, что все потоки могут видеть один и тот же порядок выполнения операций.
Модель последовательной согласованности гарантирует атомарность для всех операций записи в память, в то время как JMM не гарантирует атомарность операций чтения/записи для 64-битных переменных long и double (разделенных на две 32-битные операции записи, подробности в этой статье не имеют значения).
изменение порядка инструкций
Переупорядочивание инструкций относится к методу, принятому компилятором или процессором для оптимизации производительности при отсутствии зависимостей данных (таких как чтение после записи, чтение после записи, запись после записи) для корректировки кода. исполнительный порядок. Например:
//A
double pi = 3.14;
//B
double r = 1.0;
//C
double area = pi * r * r;
Этот код C зависит от A, B, но A, B не имеют зависимостей, поэтому код может иметь 2 порядка выполнения:
- A->B->C
- Б->А->С
Но независимо от того, какой конечный результат согласован, такая семантика, которая удовлетворяет переупорядочению в одном потоке без изменения конечного результата, называется
as-if-serial语义
, компилятор, который подчиняется семантике «как-будто-последовательно», среда выполнения и процессор вместе создают иллюзию для программистов, пишущих однопоточные программы: Однопоточные программы выполняются в порядке выполнения программы.
Решение проблемы блокировки с двойной проверкой
Оглядываясь назад на нашу проблемную процедуру блокировки с двойной проверкой, она удовлетворяетas-if-serial语义
Это правильно? Да, в однопоточном режиме все работает без проблем, а в многопоточном будут проблемы из-за переупорядочения.
Решением является известное ключевое слово volatile.Наше самое глубокое впечатление от volatile состоит в том, что оно гарантирует «видимость», а его «видимость» достигается за счет его семантики памяти:
- При записи изменяемой переменной JMM обновит значение в локальной памяти до основной памяти.
- При чтении изменяемой переменной JMM аннулирует локальную память.
Важно: чтобы обеспечить видимость семантики памяти, компилятор вставляет барьеры памяти в последовательность инструкций, чтобы предотвратить изменение порядка при генерации байт-кода!
Добавив к предыдущему коду ключевое слово volatile, можно реализовать поточно-ориентированный одноэлементный шаблон.
/**
* 单例模式三:DCL(double checked locking)双重校验锁
*/
public class Singleton3 {
private static volatile Singleton3 singleton3;
private Singleton3() {
}
public static Singleton3 getInstance() {
if (singleton3 == null) {
synchronized (Singleton3.class) {
if (singleton3 == null) {
singleton3 = new Singleton3();
}
}
}
return singleton3;
}
}
Спасибо за прочтение, если что-то получится, пожалуйста
点赞
,очень прошу关注
Пусть больше людей увидят эту статью, эта статья была впервые опубликована в технической публичной учетной записи, которая не ограничивается технологиями.Nauyus
, добро пожаловать, чтобы определить QR-код ниже, чтобы получить больше контента, в основном делиться оригинальными техническими продуктами, такими как JAVA, микросервисы, языки программирования, архитектурный дизайн, мышление и познание, и запускать режим еженедельного обновления с декабря 2019 года. Добро пожаловать, чтобы обратить внимание и присоединяйтесь к Науюс ЖЖ.
Преимущество 1: Видеоруководство по бэкенд-разработке
Десятки наборов видеоруководств по разработке серверной части JAVA, собранных за годы, включая микросервисы, распределенные, Spring Boot, Spring Cloud, шаблоны проектирования, кэширование, настройку JVM, MYSQL, крупномасштабные распределенные боевые проекты электронной коммерции и другой контент, Follow Nauyus и немедленно ответьте на [Видеоучебник] Нет никакой процедуры, чтобы получить его.
Welfare 2: Пакет и загрузка вопросов для интервью
Сводка ресурсов с вопросами для собеседований, собранных за прошедшие годы, включая руководства по поиску работы, навыки прохождения собеседований и сводку вопросов для собеседований от Microsoft, Huawei, Ali, Baidu и других компаний. В этой части еще разбираются, можете продолжать обращать внимание. Немедленно следуйте за Наюсом, чтобы ответить на [Вопросы интервью]. Нет никакой процедуры, чтобы получить это.