Встречный интервьюер | 14 схем | Никогда не бойся быть спрошенным изменчивым!

Java

Гоку Программист, который любит учиться, он самостоятельно разработал учебную платформу Java и апплет-викторину PMP. В настоящее время специализируюсь на Java, многопоточности, SpringBoot, SpringCloud, k8s. Эта официальная учетная запись не ограничивается обменом технологиями, но также использует инструменты, жизненные идеи и чтение резюме.

болтовня

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

1. Как произносится Volatile?

volatile怎么念

Я не знаю, как произнести это слово

英 [ˈvɒlətaɪl]  美 [ˈvɑːlətl]

adj. [化学] 挥发性的;不稳定的;爆炸性的;反复无常的

Так что же такое volatile в Java?

2. Для чего в Java используется volatile?

  • Volatile предоставляется виртуальной машиной Java.轻量级Механизм синхронизации (три характеристики)
    • Гарантированная видимость
    • Атомарность не гарантируется
    • Отключить перестановку инструкций

Чтобы понять три основные функции, вы должны знать модель памяти Java (JMM).Что такое JMM?

volatile怎么念

3. Что такое JMM?

Это тщательно составленная интеллектуальная карта модели памяти Java.

拿走不谢

原理图1-Java内存模型

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

Why: Скрывает различия в доступе к памяти различного оборудования и операционных систем.

JMM — это Java Memory Model, то есть Java Memory Model, или сокращенно JMM.Это само по себе абстрактное понятие и на самом деле не существует.Оно описывает набор правил или спецификаций, посредством которых каждая переменная в программе определено (включая поля экземпляра, статические поля и элементы, составляющие объект массива).

3.2 Что такое модель памяти Java?

  • 1. Определить правила доступа к различным переменным в программе
  • 2. Низкоуровневые детали хранения значений переменных в памяти
  • 3. Низкоуровневые детали выборки значений переменных из памяти

3.3 Каковы две основные памяти модели памяти Java?

原理图2-两大内存

  • основная память
    • Часть данных экземпляра объекта в куче Java
    • Память, соответствующая физическому аппаратному обеспечению
  • рабочая память
    • Часть стека Java
    • Предпочитать хранение в регистрах и кешах

3.4 Как работает модель памяти Java?

Несколько спецификаций модели памяти Java:

  • 1. Все переменные хранятся в основной памяти

  • 2. Основная память является частью памяти виртуальной машины

  • 3. У каждого потока своя рабочая память

  • 4. Рабочая память потока содержит копию переменной в основной памяти.

  • 5. Потоковые операции над переменными должны выполняться в рабочей памяти

  • 6. Разные потоки не могут напрямую обращаться к переменным в рабочей памяти друг друга.

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

Поскольку сущностью работающей программы JVM является поток, и при создании каждого потока JVM создаст для него рабочую память (в некоторых местах называемую пространством стека).Рабочая память — это частная область данных каждого потока. , а модель памяти Java предусматривает, что все переменные хранятся в основной памяти, основная память — это разделяемая область памяти, к которой имеют доступ все потоки,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写会主内存, переменными в основной памяти нельзя напрямую манипулировать.Рабочая память в каждом потоке хранит копию переменной в основной памяти, поэтому разные потоки не могут получить доступ к рабочей памяти друг друга, и связь (передача значений) между потоками должна проходить через основную память для завершения, ее краткий процесс доступа:

原理图3-Java内存模型

3.5 Три особенности модели памяти Java

  • Видимость (когда поток изменяет значение общей переменной, другие потоки немедленно узнают об изменении)
  • Атомарность (операция или серия операций неделимы и либо выполняются, либо терпят неудачу одновременно)
  • Порядок (порядок операций присваивания переменной согласуется с порядком выполнения в программном коде)

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

В-четвертых, можете ли вы привести пример использования volatile?

Рассмотрим этот сценарий:

имеет поля объектаnumberЗначение инициализации = 0, кроме того у этого объекта есть публичный методsetNumberTo100()Вы можете установить число = 100, когда основной поток вызывается дочерним потокомsetNumberTo100()Знает ли после этого основной поток, что числовое значение изменилось?

Ответ: Если volatile не используется для определения переменной number, основной поток не знает, что дочерний поток обновил значение number.

(1) Определите объект, как упомянуто выше:ShareData

class ShareData {
    int number = 0;

    public void setNumberTo100() {
        this.number = 100;
    }
}

(2) Инициализировать подпоток в основном потоке, имя которого называется子线程

Дочерний поток сначала спит в течение 3 секунд, а затем устанавливает число = 100. Основной поток непрерывно проверяет, равно ли числовое значение 0, и, если оно не равно 0, выходит из основного потока.

public class volatileVisibility {
    public static void main(String[] args) {
        // 资源类
        ShareData shareData = new ShareData();

        // 子线程 实现了Runnable接口的,lambda表达式
        new Thread(() -> {

            System.out.println(Thread.currentThread().getName() + "\t come in");

            // 线程睡眠3秒,假设在进行运算
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 修改number的值
            myData.setNumberTo100();

            // 输出修改后的值
            System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);

        }, "子线程").start();

        while(myData.number == 0) {
            // main线程就一直在这里等待循环,直到number的值不等于零
        }

        // 按道理这个值是不可能打印出来的,因为主线程运行的时候,number的值为0,所以一直在循环
        // 如果能输出这句话,说明子线程在睡眠3秒后,更新的number的值,重新写入到主内存,并被main线程感知到了
        System.out.println(Thread.currentThread().getName() + "\t 主线程感知到了 number 不等于 0");

        /**
         * 最后输出结果:
         * 子线程     come in
         * 子线程     update number value:100
         * 最后线程没有停止,并行没有输出"主线程知道了 number 不等于0"这句话,说明没有用volatile修饰的变量,变量的更新是不可见的
         */
    }
}

没有使用volatile

(3) Мы используем volatile для изменения номера переменной

class ShareData {
    //volatile 修饰的关键字,是为了增加多个线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
    volatile int number = 0;

    public void setNumberTo100() {
        this.number = 100;
    }
}

Выходной результат:

子线程	 come in
子线程	 update number value:100
main	 主线程知道了 number 不等于 0

Process finished with exit code 0

mark

Резюме: Объясните переменную, измененную с помощью volatile.Когда поток обновляет переменную, другие потоки также могут это воспринимать.

5. Почему другие потоки могут воспринимать обновления переменных?

mark

На самом деле, здесь используется протокол «Snooping». Прежде чем говорить о протоколе «Snooping», давайте сначала поговорим о Cache Coherence.

5.1 Когерентность кэша

Когда кеши, хранящиеся в нескольких ЦП, происходят из копии одной и той же основной памяти, когда другие ЦП тайно изменяют данные основной памяти, другие ЦП не знают, скопированная память будет несовместима с основной памятью, это несовместимость кеша. Так как же обеспечить когерентность кеша? Здесь операционная система должна совместно сформулировать правило синхронизации для обеспечения этого, и это правило имеет протокол MESI.

Как показано на рисунке ниже, CPU2 тайно изменяет num на 2, и num в памяти также изменяется на 2, но CPU1 и CPU3 не знают, что значение num изменилось.

原理图4-缓存一致性1

5.2 MESI

Когда ЦП записывает данные, если рабочая переменная оказывается общей переменной, т. е. копия переменной существует в других ЦП, система посылает сигнал, чтобы уведомить другие ЦП о том, что переменная памяти сохранена.缓存行Установить как недействительный. Как показано на рисунке ниже, значение num=1 в ЦП1 и ЦП3 было признано недействительным.

原理图5-缓存一致性2

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

Как показано на рисунке ниже, ЦП1 и ЦП3 обнаруживают, что кэшированное числовое значение недопустимо, повторно считывают его из памяти, и числовое значение обновляется до 2.

原理图6-缓存一致性3

5.3 Анализ шины

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

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

原理图7-缓存一致性4

5.4 Автобусный шторм

Каковы недостатки методов прослушивания шины?

Из-за протокола когерентности кэша MESI, который требует постоянного прослушивания памяти основной линии, большое количество взаимодействий может вызвать пиковую пропускную способность шины. Так что не злоупотребляйте volatile, вместо этого вы можете использовать lock, смотрите сцену~

6. Можете ли вы продемонстрировать, почему volatile не гарантирует атомарность?

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

Какая связь между этим определением и volatile, я вообще не могу понять? Покажи мне код!

Рассмотрим этот сценарий:

Когда 20 потоков одновременно увеличивают число на 1 после 1000 выполнений, каково значение числа?

В однопоточном сценарии ответ равен 20 000. Что, если это многопоточный сценарий? Ответ, вероятно, 20000, но во многих случаях меньше 20000.

Образец кода:

package com.jackson0714.passjava.threads;

/**
 演示volatile 不保证原子性
 * @create: 2020-08-13 09:53
 */

public class VolatileAtomicity {
    public static volatile int number = 0;

    public static void increase() {
        number++;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increase();
                }
            }, String.valueOf(i)).start();
        }

        // 当所有累加线程都结束
        while(Thread.activeCount() > 2) {
            Thread.yield();
        }

        System.out.println(number);
    }
}

Результат выполнения: 19144 в первый раз, 20000 во второй раз и 19378 в третий раз.

volatile第一次执行结果

volatile第二次执行结果

volatile第三次执行结果

Давайте проанализируем метод увеличения() и получим следующий ассемблерный код через инструмент декомпиляции javap:

  public static void increase();
    Code:
       0: getstatic     #2                  // Field number:I
       3: iconst_1
       4: iadd
       5: putstatic     #2                  // Field number:I
       8: return

число++ действительно выполняется3条指令:

getstatic: получить исходное значение числа iadd: добавить 1 операцию putfield: записать значение после добавления 1

Когда значение номера инструкции getstatic помещается на вершину стека операций, ключевое слово volatile гарантирует, что значение number в данный момент правильное, но при выполнении таких инструкций, как icont_1 и iadd, другие потоки могли изменить значение числа. значение числа. , а значение в верхней части стека операций становится данными с истекшим сроком действия, поэтому после выполнения инструкции putstatic меньшее значение числа может быть синхронизировано обратно в основную память.

Обобщенно следующим образом:

При выполнении строки кода number++, даже если числовая переменная изменена с помощью volatile, она может быть изменена другими потоками во время выполнения, и атомарность не гарантируется.

7. Как сделать так, чтобы результат на выходе был 20000?

7.1 синхронизированный блок кода

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

public synchronized static void increase() {
   number++;
}

synchronized同步代码块执行结果

Но использование synchronized слишком тяжело и вызовет блокировку, и только один поток может войти в этот метод. Мы можем использовать набор инструментов AtomicInterger из пакета параллелизма Java (JUC).

7.2 Атомарная операция AtomicInterger

Давайте взглянем на метод атомарного приращения AtomicInterger getAndIncrement().

AtomicInterger

public static AtomicInteger atomicInteger = new AtomicInteger();

public static void main(String[] args) {

    for (int i = 0; i < 20; i++) {
        new Thread(() -> {
            for (int j = 0; j < 1000; j++) {
                atomicInteger.getAndIncrement();
            }
        }, String.valueOf(i)).start();
    }

    // 当所有累加线程都结束
    while(Thread.activeCount() > 2) {
        Thread.yield();
    }

    System.out.println(atomicInteger);
}

Результат нескольких прогонов 20000.

getAndIncrement的执行结果

8. Что такое запрет на перегруппировку команд?

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

Как показано на рисунке ниже, порядок выполнения инструкций следующий: 1 > 2 > 3 > 4. После изменения порядок выполнения обновляется до инструкции 3->4->2->1.

原理图8-指令重排

Вам не кажется, что перестановка нарушила порядок инструкций, это хорошо?

Вспомните свои математические задачи в начальной школе:2+3-5=?, если порядок операций изменить на3-5+2=?, результат тот же. Таким образом, перестановка команд должна гарантировать, что результат программы не изменится в рамках одного потока.

8.1 Зачем переставлять

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

8.2 Какие виды пересадок бывают?

  • 1. Оптимизация компилятора: компилятор может изменить порядок выполнения операторов без изменения семантики однопоточной программы.

  • 2. Параллельное переупорядочивание на уровне инструкций. Современные процессоры используют параллелизм на уровне инструкций для перекрытия нескольких инструкций. Если зависимостей данных нет, процессор может изменить порядок, в котором операторы соответствуют машинным инструкциям.

  • 3. Переупорядочивание системы памяти. Поскольку процессор использует кэши и буферы чтения/записи, создается впечатление, что операции загрузки и сохранения выполняются не по порядку.

原理图9-三种重排

Уведомление:

  • В однопоточной среде убедитесь, что окончательный результат выполнения соответствует результату порядка кода.

  • При переупорядочивании процессор должен учитывать数据依赖性

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

8.3 Например, переупорядочивание команд в многопоточности?

Представьте себе такой сценарий: определены переменная num=0 и переменная flag=false, после того как поток 1 вызывает функцию инициализации init() для выполнения, поток вызывает метод add(), и когда другой поток оценивает flag=true, выполняется операции num+100, то ожидаемый результат состоит в том, что num будет равно 101, но из-за возможности перестановки команд порядок выполнения num=1 и flag=true может быть обратным, так что num может быть равен 100

public class VolatileResort {
    static int num = 0;
    static boolean flag = false;
    public static void init() {
        num= 1;
        flag = true;
    }
    public static void add() {
        if (flag) {
            num = num + 5;
            System.out.println("num:" + num);
        }
    }
    public static void main(String[] args) {
        init();
        new Thread(() -> {
            add();
        },"子线程").start();
    }
}

Сначала посмотрите на изменение порядка инструкций в потоке 1:

num= 1;flag = true;Порядок выполнения становится flag=true;num = 1;, как показано на временной диаграмме ниже

原理图10-线程1指令重排

Если поток 2 num=num+5 выполняется до того, как поток 1 установит num=1, то переменная num потока 2 будет иметь значение 5. Временная диаграмма показана на следующем рисунке.

原理图11-线程2在num=1之前执行

8.4 Как volatile реализует запрет на перестановку команд?

Мы определяем переменную флага с помощью volatile:

static volatile boolean flag = false;

Как добиться запрета перестановки инструкций:

Принцип: вставка до и после последовательности инструкций, сгенерированной volatile内存屏障(Memory Barries), чтобы отключить переупорядочивание процессоров.

Существует четыре типа барьеров памяти:

四种内存屏障

Как вставить барьер памяти в сценарий энергозависимой записи:

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

  • Вставляйте барьер StoreLoad (барьер записи-чтения) после каждой операции энергозависимой записи.

原理图12-volatile写的场景如何插入内存屏障

Барьер StoreStore может гарантировать, что перед записью в энергозависимую память (операция присваивания флага flag=true) все предыдущие операции обычной записи (операция присваивания num=1) были видны любому процессору, гарантируя, что все обычные операции записи сбрасываются перед записью в энергозависимую память. основная память.

Как вставить барьеры памяти в сценариях чтения volatile:

  • Вставляет барьер LoadLoad (барьер чтения-чтения) после каждой энергозависимой операции чтения.

  • Вставляет барьер LoadStore (барьер чтения-записи) после каждого чтения энергозависимой памяти.

原理图13-volatile读场景如何插入内存屏障

Барьер LoadStore может гарантировать, что все обычные операции записи (число операций присваивания num=num+5), следующие за ним, должны выполняться после чтения volatile (if(flag)).

10. Распространенные применения volatile

Вот приложение, одноэлементный режим блокировки двойного обнаружения

package com.jackson0714.passjava.threads;
/**
 演示volatile 单例模式应用(双边检测)
 * @author: 悟空聊架构
 * @create: 2020-08-17
 */

class VolatileSingleton {
    private static VolatileSingleton instance = null;
    private VolatileSingleton() {
        System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
    }
    public static VolatileSingleton getInstance() {
        // 第一重检测
        if(instance == null) {
            // 锁定代码块
            synchronized (VolatileSingleton.class) {
                // 第二重检测
                if(instance == null) {
                    // 实例化对象
                    instance = new VolatileSingleton();
                }
            }
        }
        return instance;
    }
}

Код выглядит нормально, ноinstance = new VolatileSingleton();Фактически его можно рассматривать как три псевдокода:

memory = allocate(); // 1、分配对象内存空间
instance(memory); // 2、初始化对象
instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null

Между шагами 2 и 3 нет зависимости данных, а результат выполнения программы не меняется в одном потоке до или после перестановки, поэтому такая оптимизация перестановки разрешена.

memory = allocate(); // 1、分配对象内存空间
instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null,但是对象还没有初始化完成
instance(memory); // 2、初始化对象

Если другой поток выполняется:if(instance == null) Когда он возвращает только что выделенный адрес памяти, но объект не был инициализирован, полученный экземпляр является ложным. Как показано ниже:

原理图14-双重检锁存在的并发问题

Решение: определить экземпляр как изменчивую переменную

private static volatile VolatileSingleton instance = null;

Одиннадцать, volatile не гарантирует атомарность, почему мы до сих пор его используем?

Странно то, что volatile не гарантирует атомарность, почему мы до сих пор его используем?

Volatile — это упрощенный механизм синхронизации, который оказывает меньшее влияние на производительность, чем синхронизированный.

Типичное использование: проверьте флаг состояния, чтобы определить, следует ли выйти из цикла.

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

Так почему бы нам не использовать синхронизацию и блокировку напрямую? Они гарантируют и видимость, и атомарность, почему бы их не использовать?

Поскольку synchorized и lock являются эксклюзивными блокировками (пессимистическими блокировками), если доступ к этой переменной потребуется нескольким потокам, возникнет конкуренция.Только один поток может получить доступ к этой переменной, а другие потоки будут заблокированы, что повлияет на производительность программы.

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

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

Двенадцать, разница между volatile и synchronzied

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

13. Резюме

  • Volatile гарантирует видимость: когда один поток изменяет значение общей переменной, другие потоки немедленно узнают об изменении.
  • Volatile гарантирует, что инструкции не будут переупорядочиваться в рамках одного потока: порядок выполнения инструкций гарантируется вставкой барьеров памяти.
  • volatitle не гарантирует атомарности.Операции с самоинкрементом, такие как a++, имеют риски параллелизма, такие как вычет запасов и выдача купонов.
  • 64-битные long и double переменные volatile типа, а операции чтения/записи в переменную являются атомарными.
  • Volatile может использоваться в одноэлементном режиме с двойной проверкой и имеет лучшую производительность, чем синхронизированный.
  • volatile можно использовать для проверки флага состояния, чтобы решить, выходить ли из цикла.

Использованная литература:

"Глубокое понимание виртуальной машины Java"

Искусство параллельного программирования на Java

«Практика параллельного программирования на Java»

Ждёте следующую часть? КАС вперед!

Привет, я悟空哥,"7 лет опыта разработки проектов, fullstack-инженер, руководитель группы разработчиков, очень любит базовые принципы графического программирования".

я по-прежнему手写了 2 个小程序,Java 刷题小程序,PMP 刷题小程序, нажмите на меню моей официальной учетной записи, чтобы открыть!
Кроме того, есть 111 материалов по архитектуре и 1000 вопросов для собеседования по Java, все организовано в формате PDF.
Вы можете подписаться на общедоступный номер«Архитектура чата Гоку»Ответить悟空Получите качественную информацию.

«Ретвит -> Просмотр -> Нравится -> Избранное -> Комментарий!!!»Это моя самая большая поддержка!

Серия "Параллелизм в Java должен знать, должен знать":

1. Встречный интервьюер | 14 схем | Никогда не бойся, что тебя спросят изменчиво!

2. Программист был презираем женой поздно ночью, потому что принцип CAS был слишком простым?

3. Используйте стандартные блоки, чтобы объяснить принцип ABA | Жена снова его понимает!

4. Самый тонкий во всей сети | 21 картинка показывает вам ненадежность коллекции

5.5000 слов | 24 изображения помогут вам лучше понять 21 вид блокировок в Java

6. Галантерея | Назовите 18 видов очередей на одном дыхании, интервью стабильно

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