Призрака нет, давайте купим галантерейные товары! изменчивый и синхронизированный

Java

Не по теме

Эта заметка является второй частью моей серии «Нет внутренних призраков». На самом деле, мой план состоит в том, чтобы разделить шаблоны проектирования и многопоточный параллелизм на две серии, которые будут называться «Серия Давайте учиться вместе», чтобы представить систему. Соответствующее знание, но я думал, что эта заметка была написана в прошлом году, и она была непреднамеренной и зудящей, поэтому я опубликую ее после того, как разберусь с ней. Надеюсь, все смогут ее исправить~

Также рекомендую мою предыдущую статью爆文:Призрака нет, давайте купим галантерейные товары! Оптимизация и диагностика SQL

Учитесь вместе и развивайтесь вместе!

изменчивое ключевое слово

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

  • Гарантированная видимость памяти
  • Предотвратить перестановку инструкций

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

Модель памяти JMM

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

Модель памяти JMM

Модель памяти JMM определяет, как работают потоки:即所有的共享变量都存储在主内存,如果线程需要使用,则拿到主内存的副本,然后操作一番,再放到主内存里面去

Это может навести на мысль,Является ли это основной причиной небезопасности потоков в случае многопоточного параллелизма?Если все потоки оперируют данными в основной памяти, не будет ли проблем с безопасностью потоков, и тогда возникнут следующие проблемы

Зачем нужна модель памяти JMM

Что касается этого вопроса, я чувствую себя слишком хардкорным, я могу только представить假如没有JMM,所有线程可以直接操作主内存的数据会怎么样

  • Как упоминалось выше, модель JMM не является реальной, это всего лишь спецификация, и эта спецификация может унифицировать поведение разработчиков.Если спецификации нет, может случиться так, что однократная компиляция, за которую выступает Java, будет везде круто работать.
  • Кроме того, все мы знаем, что механизм поворота кванта времени ЦП (то есть переключение процессов за очень короткое время, позволяющий пользователям наслаждаться эффектом запуска нескольких процессов без их восприятия) позволяет потокам фактически чередоваться во время выполнения. работает с деньгами. Половина операции передается потоку B, поток B изменяет сумму, а поток A, наконец, отправляется на склад с неправильными данными и т. д. Разве проблема не больше?

Поэтому я думаю, что перед лицом такого сценария предшественники подражали идее ЦП о решении когерентности кеша и определяли модель JMM (недостаточная способность, чистая спекуляция)

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

Как volatile гарантирует видимость памяти

Давайте посмотрим на кусок кода:

public class VolatileTest {
    static volatile String key;
    public static void main(String[] args){
        key = "Happy Birthday To Me!";
    }
}

При выполнении команды javap в коде для получения его байт-кода содержимое выглядит следующим образом (его можно игнорировать):

public class com.mine.juc.lock.VolatileTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#21         // java/lang/Object."<init>":()V
   #2 = String             #22            // Happy Birthday To Me!
   #3 = Fieldref           #4.#23         // com/mine/juc/lock/VolatileTest.key:Ljava/lang/String;
   #4 = Class              #24            // com/mine/juc/lock/VolatileTest
   #5 = Class              #25            // java/lang/Object
   #6 = Utf8               key
   #7 = Utf8               Ljava/lang/String;
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/mine/juc/lock/VolatileTest;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               SourceFile
  #20 = Utf8               VolatileTest.java
  #21 = NameAndType        #8:#9          // "<init>":()V
  #22 = Utf8               Happy Birthday To Me!
  #23 = NameAndType        #6:#7          // key:Ljava/lang/String;
  #24 = Utf8               com/mine/juc/lock/VolatileTest
  #25 = Utf8               java/lang/Object
{
  static volatile java.lang.String key;
    descriptor: Ljava/lang/String;
    flags: ACC_STATIC, ACC_VOLATILE

  public com.mine.juc.lock.VolatileTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/mine/juc/lock/VolatileTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc           #2                  // String Happy Birthday To Me!
         2: putstatic     #3                  // Field key:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 16: 0
        line 17: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  args   [Ljava/lang/String;
}
SourceFile: "VolatileTest.java"

Пожалуйста, обратите внимание на этот фрагмент кода:

static volatile java.lang.String key;
    descriptor: Ljava/lang/String;
    flags: ACC_STATIC, ACC_VOLATILE

Видно, что ключевое слово volatile будет активно добавлять идентификатор к переменной при компиляции:ACC_VOLATILE, это будет слишком хардовое ядро ​​(инструкция по сборке), мне может не быть хардовым (ручная собачья голова), я проведу по нему углубленное исследование в будущем, нам нужно только понимать, что ключевое слово Java volatile находится в компиляции stage Активно добавил флаг ACC_VOLATILE к переменной, чтобы гарантировать ее内存可见性

Теперь, когда volatile может гарантировать видимость памяти, есть по крайней мере один сценарий, который мы можем безопасно использовать, а именно:一写多读场景

Кроме того, не используйте System.out.println() при проверке видимости энергозависимой памяти по следующим причинам:

public void println() {
    newLine();
}

/**
 * 是不是赫然看到一个synchronized,具体原因见下文
 */
private void newLine() {
    try {
        synchronized (this) {
            ensureOpen();
            textOut.newLine();
            textOut.flushBuffer();
            charOut.flushBuffer();
            if (autoFlush)
                out.flush();
        }
    }
    catch (InterruptedIOException x) {
        Thread.currentThread().interrupt();
    }
    catch (IOException x) {
        trouble = true;
    }
}

Почему произошла перестановка инструкций?

В целях оптимизации производительности программы компилятор и процессор переупорядочат скомпилированный Java байт-код и машинные инструкции, что не повлияет на результат в случае однопоточности, однако в случае многопоточности могут возникнуть необъяснимые проблемы.

Пример перестановки инструкций

img
img

Запустив этот код, мы можем получить странный результат: полученный объект-одиночка не инициализирован. Почему это происходит? так как指令重排

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

 INSTANCE = new Singleton();

Хотя в коде всего одна строка, скомпилированные инструкции байт-кода могут быть представлены следующими тремя строками.

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

Такое изменение порядка разрешено, поскольку обмен шагами 2 и 3 не меняет результат выполнения в однопоточной среде. То есть мы указываем переменную INSTANCE на объект перед инициализацией объекта. И если другой поток просто выполняется в 2 местах, показанных в коде в это время

if (INSTANCE == null)

Затем в это время происходит интересная вещь: хотя INSTANCE указывает на неинициализированный объект, он действительно не равен нулю, поэтому это суждение вернет false, а затем вернет неинициализированный одноэлементный объект!

следующее:

img

Поскольку переупорядочение выполняется автоматически компилятором и процессором, как отключить переупорядочение инструкций?

Просто добавьте ключевое слово volatile в переменную INSTANCE, чтобы компилятор запретил переупорядочивание операций чтения и записи над volatile-переменными по определенным правилам. Скомпилированный байт-код также будет вставлять барьеры памяти в соответствующих местах.Например, барьер StoreStore и барьер StoreLoad будут вставлены до и после операции энергозависимой записи, не позволяя ЦП переупорядочивать инструкции для пересечения этих барьеров.

Теперь, когда память гарантированно видна, почему она по-прежнему небезопасна для потоков?

Хотя ключевое слово volatile гарантирует видимость памяти, возникает проблема, см. код:

index += 1;  

Эта короткая строка кода фактически разделена на несколько шагов на уровне байт-кода, таких как получение переменных, присвоение значений, вычисление и т. Д., Например, основной принцип выполнения ЦП, реальное выполнение - это одна команда, разделенная на множество шагов.

Ключевое слово volatile гарантирует, что одна операция чтения является атомарной (каждое чтение получает самое последнее значение из основной памяти).

Но, например, index += 1; по сути, это три шага, три действия, поэтому он не может гарантировать атомарность всего блока кода.

синхронизировать ключевое слово

Опровержение концепции блокировок классов

Сначала опровергните концепцию блокировки классов, синхронизация — это блокировки объектов, а объекты, заблокированные в обычных методах, статических методах и синхронизированных блоках:

тип пример кода заблокированный объект
общий метод synchronized void test() { } текущий объект
статический метод synchronized static void test() { } Блокировка — это объект класса текущего класса.
блок синхронизации void fun () { synchronized (this) {} } Замок - это объект в ()

Все согласны с тем, что в блоке синхронизированного кода объект в скобках заблокирован, поэтому см. следующий код:

public class SynDemo {

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (SynDemo.class) {
                    System.out.println("真的有所谓的类锁?");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        Thread.sleep(500);
        answer();
    }

    synchronized static void answer () {
        System.out.println("答案清楚了吗");
    }
}

// 输出结果
// 真的有所谓的类锁?
// 间隔几秒左右
// 答案清楚了吗

Так что на самом деле так называемая блокировка класса — это именно объект класса текущего класса, так что не вводите в заблуждение, синхронизация — это блокировка объекта

Принцип реализации синхронизации

JVMвходя и выходя из монитора объектов (MonitorДля достижения синхронизации методов и синхронизированных блоков

Конкретная реализация заключается в добавленииmonitor.enterИнструкции, вставленные при выходе из методов и исключенийmonitor.exitинструкция.

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

Для потока, который не получает блокировку, он будет блокироваться на входе в метод до тех пор, пока поток, который не получит блокировкуmonitor.exitТолько после этого можно предпринимать дальнейшие попытки получить блокировку.

Блок-схема выглядит следующим образом:

1566131929317
1566131929317

Пример кода:

public static void main(String[] args) {
    synchronized (Synchronize.class){
        System.out.println("Synchronize");
    }
}

Байт-код:

public class com.crossoverjie.synchronize.Synchronize {
  public com.crossoverjie.synchronize.Synchronize();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // class com/crossoverjie/synchronize/Synchronize
       2: dup
       3: astore_1
       **4: monitorenter**
       5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: ldc           #4                  // String Synchronize
      10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      13: aload_1
      **14: monitorexit**
      15: goto          23
      18: astore_2
      19: aload_1
      20: monitorexit
      21: aload_2
      22: athrow
      23: return
    Exception table:
       from    to  target type
           5    15    18   any
          18    21    18   any
}

почему дваждыmonitorexit

Синхронизированные блоки кода добавляют неявный try-finally, который вызывается в finallymonitorexitКоманда снимает блокировку, цель состоит в том, чтобы избежать ненормальной ситуации, и блокировка не может быть снята.

Несколько форм синхронизированных замков

Раньше все говорили, что вы никогда не должны использовать синхронизированный, потому что эффективность слишком низкая, но команда Hotspot провела множество оптимизаций синхронизированного, предоставив блокировки в трех состояниях: предвзятые блокировки, облегченные блокировки и тяжелые блокировки, так что производительность синхронизация будет уменьшена значительно улучшена

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

Облегченная блокировка: реализована на основе операций CAS. После того, как поток не может получить блокировку с помощью CAS, он занят ожиданием в течение определенного периода времени, что является так называемой операцией вращения. После попыток в течение определенного периода времени, но по-прежнему не удается получить блокировку, она будет обновлена ​​до тяжеловесной блокировки.

Тяжеловесная блокировка: реализована на основе базовой операционной системы.Каждый раз, когда блокировка не может быть получена, поток будет приостанавливаться напрямую, что приведет к用户态и内核态коммутация, накладные расходы на производительность относительно велики

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

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

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

Что делает синхронизированный, кроме блокировки?

  • получить блокировку синхронизации
  • очистить рабочую память
  • Копировать копию объекта из основной памяти в локальную память
  • выполнить код
  • Обновить данные основной памяти
  • снять блокировку синхронизации

Вот почему упомянутый выше System.out.println() влияет на видимость памяти.

Tips

Как получить байткод:

用法: javap <options> <classes>
其中, 可能的选项包括:
  -help  --help  -?        输出此用法消息
  -version                 版本信息
  -v  -verbose             输出附加信息
  -l                       输出行号和本地变量表
  -public                  仅显示公共类和成员
  -protected               显示受保护的/公共类和成员
  -package                 显示程序包/受保护的/公共类
                           和成员 (默认)
  -p  -private             显示所有类和成员
  -c                       对代码进行反汇编
  -s                       输出内部类型签名
  -sysinfo                 显示正在处理的类的
                           系统信息 (路径, 大小, 日期, MD5 散列)
  -constants               显示最终常量
  -classpath <path>        指定查找用户类文件的位置
  -cp <path>               指定查找用户类文件的位置
  -bootclasspath <path>    覆盖引导类文件的位置

Наконец

Спасибо следующим сообщениям в блоге и их авторам:

Интервьюер не ожидал Летучего, я мог говорить с ним полчаса

Реализация Deadly Synchronized Bottom — Введение

последний из последних

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

Лето наступило, добавляйтесь в WeChat, я угощу вас мороженым~ Только на один день 13 мая