Интервью: Чтобы зайти на Али, я перечитал Volatile и Synchronized

интервью

Добро пожаловать в публичный аккаунт [Ccww Technology Blog], оригинальная техническая статья была запущена раньше, чем блог

При глубоком понимании использования Volatile и Synchronized вы должны сначала понять модель памяти Java (JMM).


Модель памяти Java (модель памяти Java, JMM)

Модель памяти Java (JMM) представляет собой абстракцию более высокого уровня, основанную на модели аппаратной памяти, которая скрывает различия в доступе к памяти для различных аппаратных средств и операционных систем, тем самым позволяя Java-программам достигать последовательного параллелизма на различных платформах.

Внутренняя работа JMM

img

  • Основная память: хранит значения общих переменных (переменные экземпляра и переменные класса, не содержит локальных переменных, поскольку локальные переменные являются потоко-приватными, поэтому проблемы гонки нет)

  • Рабочая память: каждый поток в ЦП хранит копию общей переменной, рабочей памяти потока, поток синхронизируется с основной памятью после изменения и модификации общей переменной и обновляет значение переменной из основной памяти перед переменная читается.

  • Взаимодействие между памятью: Разные потоки не могут напрямую обращаться к переменным, не принадлежащим их собственной рабочей памяти, и передачу значений переменных между потоками необходимо выполнять через передачу основной памяти. (заблокировать, разблокировать, прочитать, загрузить, использовать, назначить, сохранить, записать)

Внутри JMM будет происходить перестановка инструкций, а также концепции af-if-serial и «произойдет до» для обеспечения правильности инструкций.

  • Для повышения производительности компиляторы и процессоры часто переупорядочивают инструкции для заданного порядка выполнения кода.
  • af-if-serial: независимо от того, как переупорядочено, результат выполнения в рамках одного потока не может быть изменен
  • Принцип Happen-before (произошло-до): существует множество принципов возникновения раньше, среди которых принцип порядка выполнения программы внутри потока выполняется в том порядке, в котором написана программа, и происходит операция, написанная впереди перед операцией, написанной в конце, если быть точным, порядок потока управления вместо порядка кода

Чтобы решить проблему непротиворечивости общих переменных в многопоточной среде, модель памяти Java включает три характеристики:

  • Атомарность: после запуска операции она будет выполняться до конца, не прерываясь другими потоками (эта операция может быть одной операцией или несколькими операциями), а атомарные операции в памяти включают чтение, загрузку, пользователя, назначение, сохранение, запись, если вам нужен более широкий диапазон атомарности, вы можете использовать synchronized для достижения операции между синхронизированными блоками.
  • Видимость: Когда поток изменяет значение общей переменной, другие потоки могут немедленно воспринять это изменение, синхронизироваться с основной памятью сразу после изменения и обновиться из основной памяти непосредственно перед каждым чтением.Вы можете использовать volatile для обеспечения видимости, или вы можно использовать ключевые слова synchronized и final.
  • Ordered: Все операции в этом потоке упорядочены, в другом потоке кажется, что все операции не по порядку, поэтому для поддержания порядка нужно использовать volatile с естественным порядком, потому что он запрещает переупорядочивание.

Разбираясь в JMM, давайте поговорим об использовании Volatile и Synchronized Какова роль Volatile и Synchronized?


Volatile

Особенности летучих:

  • Это обеспечивает видимость, когда разные потоки работают с этой переменной, то есть поток изменяет значение переменной, и новое значение сразу видно другим потокам. (для достижения наглядности)
  • Переупорядочивание инструкций отключено. (для достижения порядка)
  • volatile может гарантировать атомарность только для одного чтения/записи, i++ не гарантирует атомарность

Нестабильная видимость

При записи изменчивой переменной JMM обновит значение общей переменной в рабочей памяти, соответствующей потоку, и обновит его в основной памяти.

При чтении изменчивой переменной JMM сделает недействительной рабочую память, соответствующую потоку, и поток будет считывать общую переменную из основной памяти.

Операция записи:

Операция чтения:

Volatile запрещает переупорядочивание инструкций

JMM использует стратегию вставки барьера памяти для запрета перестановки изменчивых инструкций:

Вставьте барьер StoreStore перед каждой операцией энергозависимой записи. Вставьте барьер StoreLoad после каждой энергозависимой записи

Вставьте барьер LoadLoad после каждого чтения volatile. Вставьте барьер LoadStore после каждого чтения volatile

Synchronized

Синхронизация — это один из наиболее распространенных способов решения проблем параллелизма в Java, а также самый простой способ. Есть три основные функции Synchronized:

  • Атомарность: доступ к коду синхронизации, обеспечивающему взаимное исключение потоков;
  • Видимость: чтобы гарантировать, что изменение общих переменных можно увидеть вовремя, фактически, через модель памяти Java, «перед разблокировкой переменной она должна быть синхронизирована с основной памятью; если переменная заблокирована, рабочая память будет очищено. Значение этой переменной в, прежде чем исполнительный механизм использует эту переменную, ему необходимо повторно инициализировать значение переменной из операции загрузки основной памяти или операции назначения», чтобы гарантировать
  • Упорядочивание: эффективно решить проблему переупорядочивания, то есть «операция разблокировки происходит до операции блокировки той же блокировки»;

Synchronized имеет в общей сложности три использования:

  1. Когда синхронизация действует на метод экземпляра, блокировка монитора (монитор) является экземпляром объекта (этот);
  2. Когда синхронизация действует на статический метод, блокировка монитора (монитор) является экземпляром класса объекта, поскольку данные класса существуют в постоянном поколении, поэтому блокировка статического метода эквивалентна глобальной блокировке класса;
  3. Когда синхронизация действует на экземпляр объекта, блокировка монитора (монитор) — это экземпляр объекта, заключенный в круглые скобки;

Более подробный анализ см.Синхронизированный параллелизм Java

После понимания Volatile и Synchronized давайте посмотрим, как использовать Volatile и Synchronized для оптимизации одноэлементного шаблона.


Оптимизация одноэлементного режима — двойное обнаружение DCL (Double Check Lock)

Давайте сначала посмотрим на одноэлементный шаблон общего шаблона:

class Singleton{
    private static Singleton singleton;    
    private Singleton(){}

    public static Singleton getInstance(){
            if(singleton == null){
                singleton = new Singleton();   // 创建实例
        }
        return singleton;
    }

}

Могут возникнуть проблемы: когда есть два потока A и B,

  • Судьи темы Aif(singleton == null)Поток зависает при подготовке к выполнению экземпляра create,
  • В это время поток B также решит, что синглтон пуст, и затем выполнит создание экземпляра объекта для возврата;
  • Наконец, поскольку поток A вошел, также будет создан объект-экземпляр, что приводит к ситуации с несколькими одноэлементными объектами.

Первое, что приходит на ум, это использовать синхронизированные статические методы:

public class Singleton {
    private static Singleton singleton;
    private Singleton(){}
    public static synchronized Singleton getInstance(){
        if(singleton == null){
       		 singleton = new Singleton();
        }
        return singleton;
    }
}

Хотя это простое и грубое решение, оно сделает этот метод относительно неэффективным, что приведет к серьезному снижению производительности программы.Есть ли другие решения получше?

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

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

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

class Singleton{
    private static Singleton singleton;    
    private Singleton(){}
    
    public static Singleton getInstance(){
        if(singleton == null){
            synchronized(Singleton.class){
                if(singleton == null)
                    singleton = new Singleton();   
            }
        }
        return singleton;
    }
}

Хотя это оптимизационное решение решает проблему создания только одного экземпляра, из-за наличия перестановки инструкций оно будет небезопасным при многопоточности (когда происходит перестановка, последующие потоки обнаруживают, что синглтон не равен нулю, и используют его напрямую, непредвиденные проблемы может возникнуть.). причинаsingleton = new Singleton()Создание нового объекта проходит в три этапа:

  • 1. Распределение памяти
  • 2. Инициализация
  • 3. Вернуть ссылку на объект

Из-за изменения порядка шаги 2 и 3 могут быть изменены, и процесс выглядит следующим образом:

  • 1. Выделить место в памяти
  • 2. Назначьте адрес области памяти соответствующей ссылке
  • 3. Инициализировать объект

Итак, проблема найдена, как ее решить? Это запрещает переупорядочивание на шагах 2 и 3 фазы инициализации, просто Volatile запрещает переупорядочивание инструкций, так что двойное обнаружение действительно работает.

public class Singleton {
    //通过volatile关键字来确保安全
    private volatile static Singleton singleton;
    private Singleton(){}
    public static Singleton getInstance(){
        if(singleton == null){
           synchronized (Singleton.class){
                if(singleton == null){
                singleton = new Singleton();
            }
        }
    }
    return singleton;
    }
}

Наконец-то появился наш идеальный одноэлементный шаблон с двойным обнаружением.


Суммировать

  • Суть volatile в том, чтобы сообщить jvm, что значение текущей переменной в регистре (рабочей памяти) неопределенно и его нужно считать из основной памяти; synchronized блокирует текущую переменную, доступ к переменной может получить только текущий поток, и другие темы заблокированы.
  • volatile можно использовать только на уровне переменной; synchronized можно использовать на уровне переменной, метода и класса.
  • Volatile может обеспечить только видимость модификации переменных и не может гарантировать атомарность; в то время как синхронизация может гарантировать видимость модификации и атомарность переменных
  • Volatile не приведет к блокировке потока; синхронизированный может привести к блокировке потока.
  • Переменные, помеченные как volatile, не будут оптимизированы компилятором; переменные, помеченные как synchronized, могут быть оптимизированы компилятором.
  • Единственный безопасный случай использования volatile вместо synchronized — это когда в классе есть только одно изменяемое поле.

Вы в порядке, офицеры? Если вам это нравится, проведите пальцем, чтобы нажать 💗, нажмите, чтобы подписаться! ! Спасибо за Вашу поддержку!

Добро пожаловать в паблик-аккаунт [Ccww Technology Blog], оригинальные технические статьи будут запущены в ближайшее время