Не по теме
Эта заметка является второй частью моей серии «Нет внутренних призраков». На самом деле, мой план состоит в том, чтобы разделить шаблоны проектирования и многопоточный параллелизм на две серии, которые будут называться «Серия Давайте учиться вместе», чтобы представить систему. Соответствующее знание, но я думал, что эта заметка была написана в прошлом году, и она была непреднамеренной и зудящей, поэтому я опубликую ее после того, как разберусь с ней. Надеюсь, все смогут ее исправить~
Также рекомендую мою предыдущую статью爆文
:Призрака нет, давайте купим галантерейные товары! Оптимизация и диагностика SQL
Учитесь вместе и развивайтесь вместе!
изменчивое ключевое слово
Ключевое слово volatile — это пункт, который часто задают в общих интервью, и каждый отвечает на него не более чем двумя пунктами:
- Гарантированная видимость памяти
- Предотвратить перестановку инструкций
Для того, чтобы быть более уверенным, то давайте посмотрим глубже.
Модель памяти JMM
Когда мы говорим о ключевом слове volatile, нам сначала нужно понять модель памяти 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 байт-код и машинные инструкции, что не повлияет на результат в случае однопоточности, однако в случае многопоточности могут возникнуть необъяснимые проблемы.
Пример перестановки инструкций
Запустив этот код, мы можем получить странный результат: полученный объект-одиночка не инициализирован. Почему это происходит? так как指令重排
Прежде всего, должно быть понятно, что код в блоке синхронизированного кода также может быть переупорядочен командой. Тогда посмотрите на суть проблемы
INSTANCE = new Singleton();
Хотя в коде всего одна строка, скомпилированные инструкции байт-кода могут быть представлены следующими тремя строками.
- 1. Выделить место в памяти для объекта
- 2. Инициализировать объект
- 3. Укажите переменную INSTANCE на только что выделенный адрес памяти.
Такое изменение порядка разрешено, поскольку обмен шагами 2 и 3 не меняет результат выполнения в однопоточной среде. То есть мы указываем переменную INSTANCE на объект перед инициализацией объекта. И если другой поток просто выполняется в 2 местах, показанных в коде в это время
if (INSTANCE == null)
Затем в это время происходит интересная вещь: хотя INSTANCE указывает на неинициализированный объект, он действительно не равен нулю, поэтому это суждение вернет false, а затем вернет неинициализированный одноэлементный объект!
следующее:
Поскольку переупорядочение выполняется автоматически компилятором и процессором, как отключить переупорядочение инструкций?
Просто добавьте ключевое слово 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
Только после этого можно предпринимать дальнейшие попытки получить блокировку.
Блок-схема выглядит следующим образом:
Пример кода:
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 мая