Замок

Java

предисловие

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

Основная причина проблем с безопасностью потоков

1. Наличие общих данных (критические ресурсы)

2. Существует несколько потоков для совместной работы с данными.

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

Synchronized

  • Особенности мьютекса

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

    Для Java ключевое слово Synchronized может удовлетворять двум вышеуказанным характеристикам.
    Примечание: Синхронизированные замки не кодовые, аобъект.

  • Синхронизированная классификация захвата замков

    • блокировка объекта

      Есть два способа получить блокировку объекта:
      1.Блок синхронизированного кода (синхронизированный (этот), синхронизированный (объект экземпляра класса)), замок — это объект в круглых скобках.
      2.Синхронизированный нестатический метод (синхронизированный метод), блокировка является экземпляром текущего объекта.

    • блокировка класса

      Есть два способа получить блокировку класса:
      1.Блок синхронизированного кода (синхронизированный (object.getClass()), синхронизированный (имя класса.класс)), замок — это объект класса (class object) объекта, заключенного в круглые скобки.
      2.синхронизированный статический метод, блокировка является объектом класса текущего объекта.

  • Проверка блокировки объекта и блокировки класса

    • код

      ThreadDemo.java:

      public class ThreadDemo implements Runnable{
          public void asynMethod() {
              System.out.println(Thread.currentThread().getName()+" 开始运行(异步方法)");
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  // TODO Auto-generated catch block
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName()+" 结束运行(异步方法)");
          }
          public synchronized void syncMethod() {
              System.out.println(Thread.currentThread().getName()+" 开始运行(同步方法)");
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  // TODO Auto-generated catch block
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName()+" 结束运行(同步方法)");
          }
          public void syncBlock() {
              synchronized(this) {
                  System.out.println(Thread.currentThread().getName()+" 开始运行(同步代码块)");
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      // TODO Auto-generated catch block
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName()+" 结束运行(同步代码块)");
              }
          }
          public void syncClass() {
              synchronized (this.getClass()) {
                  System.out.println(Thread.currentThread().getName()+" 开始运行(以class文件为锁对象的同步代码块)");
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      // TODO Auto-generated catch block
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName()+" 结束运行(以class文件为锁对象的同步代码块)");
              }
          }
          public static synchronized void syncStaticMethod() {
              System.out.println(Thread.currentThread().getName()+" 开始运行(静态同步方法(以class文件为锁))");
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  // TODO Auto-generated catch block
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName()+" 结束运行(静态同步方法(以class文件为锁))");
          }
          @Override
          public void run() {
          	// TODO Auto-generated method stub
              if(Thread.currentThread().getName().startsWith("ASYN")) {
                  asynMethod();
              }else if(Thread.currentThread().getName().startsWith("SYNC_METHOD")) {
                  syncMethod();
              }else if(Thread.currentThread().getName().startsWith("SYNC_BLOCK")) {
                  syncBlock();
              }else if(Thread.currentThread().getName().startsWith("SYNC_CLASS")) {
                  syncClass();
              }else if(Thread.currentThread().getName().startsWith("SYNC_STATIC")) {
                  syncMethod();
              }
          }
      }
      

      ThreadTest.java:

      public class ThreadTest {
          public static void main(String[] args) {
              ThreadDemo threadDemo = new ThreadDemo();
              Thread thread1 = new Thread(threadDemo,"ASYN_Thread1");
              Thread thread2 = new Thread(threadDemo,"ASYN_Thread2");
              Thread thread3 = new Thread(threadDemo,"SYNC_METHOD_Thread1");
              Thread thread4 = new Thread(threadDemo,"SYNC_METHOD_Thread2");
              Thread thread5 = new Thread(threadDemo,"SYNC_BLOCK_Thread1");
              Thread thread6 = new Thread(threadDemo,"SYNC_BLOCK_Thread2");
              Thread thread7 = new Thread(threadDemo,"SYNC_STATIC_Thread1"); 
              Thread thread8 = new Thread(threadDemo,"SYNC_STATIC_Thread2"); 
              thread1.start();
              thread2.start();
              thread3.start();
              thread4.start();
              thread5.start();
              thread6.start();
              thread7.start();
              thread8.start();
          }
      }
      
    • Результаты теста

      同步测试
      Это также видно из приведенного выше теста: один и тот же классМежду блокировками объекта класса и блокировкой объекта экземпляра нет корреляции..

  • Обзор блокировок объектов и блокировок классов

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

    2. Если блокировка является тем же объектом, поток обращается к объектуБлок синхронизированного кодакогда другой обращается к объектуБлок синхронизированного кодаТема будет заблокирована.

    3. Если блокировка является тем же объектом, поток обращается к объекту.Синхронный методкогда другой объект доступаСинхронный методТема будет заблокирована.

    4. Если блокировка является тем же объектом, поток обращается к объекту.Блок синхронизированного кодакогда другой объект доступаСинхронный методТема будет заблокирована, и наоборот.

    5. Объектные блокировки разных объектов одного класса не мешают друг другу.

    6. Поскольку классовая блокировка также является специальной блокировкой объекта, ее эффективность согласуется с указанными выше 1, 2, 3 и 4. Поскольку класс имеет только одну блокировку объекта, использование блокировок класса разными объектами одного и того же класса будет синхронным.

    7. Блокировки классов и блокировки объектов не мешают друг другу.

Реализация основного принципа Synchronized

После краткого понимания Synchronized мы углубимся в основные принципы Synchronized.

  • Основы реализации Synchronized

    • Заголовок объекта Java

      В виртуальной машине Hotspot расположение объектов в памяти разделено на следующие три части:
      1.заголовок объекта
      2. Данные экземпляра
      3. Выровняйте отступы
      Так как я не очень разбираюсь в данных инстанса и его заполнении, и у меня не очень хорошее отношение к этой части, я пока не буду упоминать об этом.Расскажите подробно о голове объекта:
      Заголовок объекта разделен на две части:
      1.Mark Word: Хэш-код, возраст поколения, тип блокировки, флаг блокировки и другая информация об объекте сохраняются по умолчанию.
      2.Class Metadata Address: Указатель типа указывает на метаданные класса объекта, и JVM использует этот указатель, чтобы определить, к какому классу относится объект.
      Поскольку информация заголовка объекта является дополнительными данными, не имеющими никакого отношения к работе объекта, учитывая проблему эффективности работы, MarkWord разработан как нефиксированная структура данных, чтобы хранить больше достоверных данных, он будет в зависимости от состояния самого объекта повторно используйте собственное пространство для хранения. ​

      对象头

    • Монитор (Блокировка монитора)

      С момента своего рождения объекты Java инкапсулировали внутри невидимый замок —Модитор (блокировка монитора), мы можем использовать его как инструмент синхронизации. Существуют различные отношения между монитором и объектами, например, он может быть сгенерирован вместе с объектом или может быть сгенерирован автоматически, когда объекту необходимо получить блокировку. Конструктор Monitor (C++) выглядит следующим образом:

      ObjectMonitor() {
          _header       = NULL;
          _count        = 0;
          _waiters      = 0,
          _recursions   = 0;
          _object       = NULL;
          _owner        = NULL;//锁的持有者
          _WaitSet      = NULL;
          _WaitSetLock  = 0 ;
          _Responsible  = NULL ;
          _succ         = NULL ;
          _cxq          = NULL ;
          FreeNext      = NULL ;
          _EntryList    = NULL ;
          _SpinFreq     = 0 ;
          _SpinClock    = 0 ;
          OwnerIsThread = 0 ;
          _previous_owner_tid = 0;
       }
      

      Сначала запомните эти свойства (сверху вниз):считать (счетчик),object,владелец (поток, в настоящее время удерживающий блокировку),WaitSet (пул ожидания),_EntryList (пул блокировки).

      Как видно из исходного кода, в Мониторе две очереди, однаWaitSet, другой _EntryList, две очередипул ожиданияа такжезамок пул, когда несколько потоков хотят получить блокировку, они входят вместеЗаблокировать пул (EntryList), только один поток может получить блокировку, а другие потоки должны ожидать в пуле блокировок, и поток, который успешно получит блокировку, войдет в _objectобласть и поместите _ в мониторownerАтрибут меняется на текущий поток, а счетчик в Monitor_countдобавит один. Когда поток вызывает метод wait(),ownerсвойство будет установлено в null,countОн также уменьшится на единицу, а текущий поток войдет в _WaitSetждет, когда его разбудят.

      monitor锁竞争获取释放

      С этой точки зрения блокировка Monitor существует в заголовке объекта каждого объекта Java, а ключевое слово Synchronized получает блокировку таким образом, поэтому любой объект в Java может использоваться в качестве блокировки.

  • Понимание синхронизации с уровня байт-кода

    • Предварительная подготовка (код)

      public class ThreadClassDemo {
      	public void syncPrint() {
              //同步代码块
          	synchronized(this) {
              	System.out.println("hello——sync block");
              }
          }
          public synchronized void syncMethodPrint() {
          	System.out.println("hello——sync method");
          }
      }
      
    • Просмотр файлов байт-кода с помощью команды javap

      Найдите каталог, в котором находится файл класса, в каталоге кода Java, который вы только что написали, и используйтеjavap -verbose ThreadClassDemo.classКоманда для просмотра файла байт-кода.

      Файл байт-кода с использованием синхронизированных блоков кода

      字节码文件—同步代码块

      Файл байт-кода с использованием синхронизированных методов:

      字节码文件—同步方法

      Мы видим, что monitorenter и monitorexit явно используются в байт-коде с использованием синхронизированных блоков кода для представления блокировки и снятия блокировок, в то время как в байт-коде с использованием синхронизированных методов нет monitorenter и monitorexit. На самом деле в процессе синхронизации метода синхронизации monitorenter и monitorexit не отображаются. ТакКак достигается синхронизация?? Мы видим, что в файле байт-кода синхронизированного метода есть атрибут,flags, которая включает в себяACC_SYNCHRONIZED, этот флаг может определить, является ли метод синхронным методом.Когда метод вызывается, система определяет, имеет ли метод флаг ACC_SYNCHRONIZED, затем поток, выполняющий метод, будет удерживать монитор, затем выполнение завершается, и, наконец, монитор освобожден.

  • Небольшой разговор о Synchronized

    Узнав о Synchronized раньше, кто-то всегда говорил, что Synchronized — это тяжеловесная блокировка. Действительно, в более ранних версиях (до Java 1.6) Synchronized была тяжеловесной блокировкой, потому что в основном полагалась на реализацию MutexLock, и при каждой блокировке требовалось переключаться из пользовательского состояния в базовое, что является проблемой для Процессор Тяжелая работа. В сценарии с высокой степенью параллелизма нереально выполнять преобразование пользовательского режима в базовый режим каждый раз при добавлении блокировки, поэтому позже я буду сравнивать ReentrantLock с Synchronized. Но сегодняшний день отличается от прошлого.После JDK1.6 производительность Synchronized значительно улучшилась, и это уже не тяжеловесная блокировка, которую презирали в то время.

  • Некоторые оптимизации, сделанные Hotspot на блокировках после JDK1.6

    • блокировка спина

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

    • Адаптивная блокировка спина

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

    • снятие блокировки

      Устранение блокировок — еще одна стратегия оптимизации виртуальной машины.Во время процесса JIT-компиляции некоторые блокировки, которые, по мнению JVM, не столкнутся с конкуренцией, автоматически устраняются, то есть когда JVM определяет, что заблокированный ресурс является ресурсом, которым нельзя поделиться, снять его замок.

    • блокировка огрубления

      public class Test{
      	public static void main(String[] args){
      		StringBuffer sb = new StringBuffer();
      		int i = 0;
      		while(i<100){
      			sb.append("test")
      			i++;
      		}
      	}
      }
      

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

  • Четыре состояния синхронизации

    Synchronized имеет четыре состояния, которыенет замка,Блокировка смещения,Легкий замок,тяжелый замок, автоматическое обновление или понижение версии в соответствии с различными сценариями.Направление обновления:Без блокировки --> Блокировка смещения --> Легкая блокировка --> Тяжелая блокировка.

    • нет замка

      Хорошо понятно, что нет блокировки, нет блокировки, асинхронность, гм.

    • Блокировка смещения

      Основное назначение смещенных замков состоит в том, чтобы:Сокращение затрат на приобретение замков тем же потоком. В большинстве случаев многопоточная конкуренция за блокировку отсутствует, и она всегда повторно получается одним и тем же потоком. Чтобы решить эту проблему, хотспоты вводят смещенную блокировку во время оптимизации.Основная идея заключается в следующем: если поток получает блокировку, то блокировка находится в смещенном режиме, и структура Mark Word становитсяСтруктура блокировки смещения(Флаг блокировки равен 1 01, подробности см. в диаграмме структуры заголовка объекта выше), когда поток снова запрашивает блокировку, нет необходимости выполнять какие-либо операции синхронизации, просто проверьте Mark WordФлаг блокировки является смещенной блокировкойа такжеТекущий идентификатор потока равен идентификатору потока в Mark Word.Вот именно, что сокращает работу большого количества приложений блокировки.

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

      Что касается предвзятой блокировки, я также видел объяснение, которое легче понять:

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

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

      Ссылка на ссылку:Смещенная блокировка Java, облегченная блокировка и синхронизированный принцип тяжеловесной блокировки

    • Легкий замок

      Облегченные блокировки обновляются со смещенными блокировками. Смещенная блокировка запускается, когда поток входит в синхронизированный блок. Когда второй поток вступает в соревнование замков, смещенная блокировка будет обновлена ​​до упрощенной блокировки. (Если поток, который в настоящее время удерживает блокировку, все еще жив, он будет обновлен, в противном случае он продолжит отдавать предпочтение потоку, который в настоящее время конкурирует за блокировку).Облегченные замки подходят для случая, когда потоки выполняются попеременно и блокируются попеременно., если несколько потоков одновременно конкурируют за одну и ту же блокировку, облегченная блокировка будет заменена на тяжеловесную.

      Процесс запирания облегченного замка:

      1. Когда код входит в выполнение блока синхронизации, если состояние блокировки объекта синхронизации является состоянием без блокировки, виртуальная машина сначала устанавливает пространство с именем Lock Record в кадре стека текущего потока, которое используется для хранения текущего слова метки объекта блокировки.Копия, официально называемая словом Displaced Mark Word.

      2. Скопируйте Mark Word из заголовка объекта в Lock Record.

      3. После успешного копирования виртуальная машина будет использовать операцию CAS, чтобы попытаться обновить слово метки объекта до указателя на запись блокировки и указать указатель _owner в записи блокировки на слово метки объекта. обновление прошло успешно, перейдите к шагу 4, в противном случае перейдите к шагу 5.

      4. Если операция обновления выполнена успешно, то поток владеет блокировкой объекта, а флаговый бит слова метки объекта устанавливается в 00, указывая на то, что объект находится в заблокированном состоянии облегченной блокировки.

      5. Если операция обновления завершится неудачно, виртуальная машина сначала проверит, указывает ли MarkWord объекта на фрейм стека текущего потока, если да, то это означает, что текущий поток уже владеет блокировкой объекта, а затем может напрямую войти в блок синхронизации для продолжения выполнения, в противном случае это означает, что несколько потоков конкурируют за блокировки, и облегченная блокировка будет обновлена ​​до тяжелой блокировки. Значение состояния флага блокировки становится равным 10. Указатель на тяжеловесную блокировку (мьютекс ) хранится в Mark Word, и блокировка ожидает блокировки позже.Поток также входит в заблокированное состояние, и текущий поток пытается использовать вращение для получения блокировки.

      轻量级锁加锁

    • тяжелый замок

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

    锁等级总结

  • Семантика памяти замков

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

Синхронизированный и ReentrantLock

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

Знания, связанные с AQS: CAS, спин-блокировка, парковка, разпарковка

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

  • О ReentrantLock

    блокировка повторного входа(мне не особо нравится этот перевод, ниже он еще называется ReentrantLock), его семантика в основном такая же, как Synchronized, основанная наAQSреализованоDoug LeaОн был написан Дашеном и был представлен в версии Java 1.5.Болезнь, которую изначально хотели решить, заключалась в том, что каждый раз, когда Synchronized блокируется, необходимо выполнять утомительное переключение пользовательского режима на режим ядра (до Java 1.6), что приводит к трата ресурсов. И блокировка ReentrantLock находится вАльтернативное выполнение потоковВ этом сценарии это можно сделать полностью на уровне JVM, без необходимости вызывать метод Native, а затем выполнять системный вызов каждый раз, когда добавляется блокировка, например Synchronized, что позволяет избежать частого переключения состояния пользователя и ядра и повышает скорость.

  • Процесс блокировки ReentrantLock

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

    Я предполагаю, что для получения блокировки есть три потока A, B и C. Исходя из этого сценария, я расскажу о процессе блокировки ReentrantLock (весь процесс я напишу в комментариях к коду, а там будет быть некоторые прыжки в середине. место, чтобы посмотреть).

    • Сначала поймите структуру AQS

      /*AQS是由Node构成的,每个Node中保存了当前线程的前驱节点、后继节点,同步状态、等待状态。当然,还包含了一个线程实体*/
      
      //同步队列的头节点
      private transient volatile Node head;
      //同步队列的尾节点
      private transient volatile Node tail;
      //锁是否被占用,0表示自由,1表示被占用
      private volatile int state;
      
      

      Поскольку ReentrantLock реализован на основе AQS (ReentrantLock использует внутреннюю синхронизацию, которая является подклассом AQS.), поэтому вы должны сначала понять, как выглядит структура AQS.

    • ReentrantLock честный и несправедливый

      public ReentrantLock(boolean fair) {
          //构造方法
          //如果传入的是true,则创建一个公平锁,如果传入false,创建一个非公平锁
          //默认是非公平锁
          //非公平锁和公平锁,都继承自ReentrantLock的静态内部类Sync,Sync继承自AQS
      	sync = fair ? new FairSync() : new NonfairSync();
      }
      
    • Блокировки потока (сначала поговорим о процессе блокировки честных блокировок)

      1. ПервыйТемаПопытка получить блокировку (вызовите метод reentrantLock.lock())

      //线程A获取锁
      public void lock() {
      	sync.lock();
      }
      

      2. Введитечестный замокметод блокировки()

      final void lock() {
          /*公平锁的lock()方法实际上封装的是acquire方法,而非公平锁是直接进行CAS操作尝试获取锁
            如果获取锁失败,调用acquire方法,看到这的同学跳转到第3点,acquire方法。
          */
      /*==================================分割线==================================*/
      	acquire(1);
          /*根据3,acquire方法正常返回,于是lock执行结束,正常返回。*/
      }
      

      3. Введите метод получения

      public final void acquire(int arg) {
      /*首先调用tryAcquire方法尝试获取锁,为了方便,我直接把tryAcquire方法贴在下方
       *(看到这里的小伙伴直接跳到下方代码tryAcquire方法代码
       */
      /*==================================分割线==================================*/
      	if (!tryAcquire(arg) &&
              acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
          	selfInterrupt();
          /*根据下方代码,tryAcquire方法返回结果:true,这个if语句中将true取反,变为false,
           *于是这个if语句不会被执行,acquire方法直接返回。
           */
      }
          
      //从acquire方法进入tryAcquire方法
      protected final boolean tryAcquire(int acquires) {
      	//获取当前线程:线程A
      	final Thread current = Thread.currentThread();
      	//获取当前state状态,state是锁是否被占用的标志位,0表示自由,1表示被占用
          //当前只有A线程开始尝试获取锁,那么锁肯定是自由的,state == 0,即c == 0、
          int c = getState();
          if (c == 0) {
          //此时c一定等于0,于是到这里
          //有一个hasQueuedPredecessors方法,这个方法是判断是否等待队列中有排在自己之前的元素。源代码我也贴在下方。(看到这的同学直接跳到第4点看hasQueuedPredecessors方法)
      /*==================================分割线==================================*/
          //根据下方第4点结果说明,hasQueuePredecessors返回false,这个if语句对这个结果取反,就变成了true。
          //可以继续执行下一个条件:campareAndSetState(0,acquires),即CAS。
          //CAS操作将state由0改为1,说明锁此时被占用,那么被谁占用了呢?
          //执行setExclusiveOwnerThread(current),将锁的持有者改为当前线程。
          //整个方法返回 true。
          if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
          	setExclusiveOwnerThread(current);
              return true;
      	}
      }
          else if (current == getExclusiveOwnerThread()) {
              int nextc = c + acquires;
              if (nextc < 0)
              	throw new Error("Maximum lock count exceeded");
              setState(nextc);
              return true;
          }
          return false;
      }
      

      4. Введите метод hasQueuedPredecessors

      public final boolean hasQueuedPredecessors() {
      	//将AQS的队尾元素赋值给t
          Node t = tail; 
          //将AQS的队头元素赋值给h
          Node h = head;
          Node s;
          /*A线程会进到这里,首先判断h是不是等于t
           *线程A是第一个到达这里的,此时队列并没有被初始化,所以h == null,t==null,
           *所以此时h != t为false,这个方法直接返回false
          */
          return h != t &&
          ((s = h.next) == null || s.thread != Thread.currentThread());
      }
      

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

    • Процесс блокировки потока B

      Старое правило заключается в том, чтобы сначала опубликовать код.нить БВведите метод блокировки, чтобы попытаться удержать блокировку.
      1. Поток B входит в метод блокировки

      final void lock() {
          /*公平锁的lock()方法实际上封装的是acquire方法,而非公平锁是直接进行CAS操作尝试获取锁
            如果获取锁失败,调用acquire方法。
          */
      acquire(1);
      }
      

      2. Поток B входит в метод получения

      public final void acquire(int arg) {
          /*线程B首先调用tryAcquire方法尝试获取锁
           *(看到这里的小伙伴直接跳到3,tryAcquire方法)
      /*==================================分割线==================================*/
          */
          if (!tryAcquire(arg) &&
              acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
              selfInterrupt();
          /*根据下方代码,tryAcquire方法返回结果:false,这个if语句中将false取反,变为true,
           *于是这个if语句会继续往下判断,调用acquireQueue方法,而调用acquireQueue方法之前,会
           *先调用addWaiter方法,看到这的同学跳到4,进入addWaiter方法
          */
      /*==================================分割线==================================*/
      /*根据4的结果,addWaiter方法返回了一个保存当前线程,即线程B的Node对象*/
      /*==================================分割线==================================*/
      /*于是继续执行acquireQueued方法,跳到6*/
          
      }
      

      3. Поток B входит в метод tryAcquire

      //线程B从acquire方法进入tryAcquire方法
      protected final boolean tryAcquire(int acquires) {
          //获取当前线程:线程B
          final Thread current = Thread.currentThread();
          //获取当前state状态,state是锁是否被占用的标志位,0表示自由,1表示被占用
          //当前线程A正在持有锁,所以state==1,即c==1
          int c = getState();
          if (c == 0) {
              //此时c==1,于是不能到达这里
              if (!hasQueuedPredecessors() &&
                  compareAndSetState(0, acquires)) {
                  setExclusiveOwnerThread(current);
                  return true;
              }
          }
          else if (current == getExclusiveOwnerThread()) {
              //判断当前线程是否为持有锁的线程
              //显然线程B不是当前持有锁的线程,于是也到不了这里。
              int nextc = c + acquires;
              if (nextc < 0)
                  throw new Error("Maximum lock count exceeded");
              setState(nextc);
          return true;
          }
      //线程B整个tryAcquire方法返回false
          return false;
      }
      

      4. Поток B входит в метод addWaiter

      private Node addWaiter(Node mode) {
          //创建一个Node,并将其中的thread属性改为当前线程,也就是线程B
          //Node中保存了当前线程,还保存了当前线程的前驱节点、后继节点,同步状态、等待状态。AQS是由Node组成的
          Node node = new Node(Thread.currentThread(), mode);
          //将tail赋值给pred,但此时tail为null
          Node pred = tail;
          if (pred != null) {
              //此时pred显然为null,因为等待队列并没有被初始化,所以执行enq方法
              node.prev = pred;
              if (compareAndSetTail(pred, node)) {
                  pred.next = node;
                  return node;
              }
          }
          //执行enq方法,跳到5,enq方法
      enq(node);
      /*==================================分割线==================================*/
      //enq方法返回
          //整个addWaiter方法返回,返回保存了当前线程的node
          return node;
      }
      

      5. Поток B входит в метод enq

      private Node enq(final Node node) {
          //enq方法传入一个node,即保存了线程B的node
          for (;;) {//死循环
              //第一次循环:为正在运行的那个Thread创建一个Node,该Node的thread属性为null
              //第二次循环:将保存了线程B的Node的prev指针指向第一次循环创建的Node
              //最后形成一个队列,队列中的队首是保存了正在运行的线程(线程A)的Node,而紧随其后的就是线程B的Node
              Node t = tail;
              if (t == null) { 
                  /*第一次循环进入这块代码:
                  这个CAS非常有意思,首先它是一个设置AQS队首的CAS操作,而队首等于新建的一个Node,只有当设置成功了,将队首赋值给队尾*/
                  /*这个操作正好是进行了一次初始化
                   *于是现在队列应该长这样:
                   *队列中只有一个元素,
                   *而head和tail同时指向同一个Node
                   */
                  //然后enq方法进入第二次循环
              	if (compareAndSetHead(new Node()))
                      tail = head;
              } else {
                  /*第二次循环进入这块代码
                   *将t赋值给node的prev,此时t==head,所以就是将head赋值给t的prev,node是什么?node就是保存了线程B的那个节点,也就是,保存了线程B的指向上一个节点的指针prev,让它指向我们新创建的这个node。
                   *然后CAS更新队尾,将队尾从刚才的指向队首,变为指向当前线程node
                   *最后返回这个队列t
                  */
                  node.prev = t;
                  if (compareAndSetTail(t, node)) {
                  	t.next = node;
                      return t;
              	}
              }
          }
      }
      

      6. Поток B входит в методAcquireQueued

      final boolean acquireQueued(final Node node, int arg) {
      	//该方法传入一个node,即保存了当前线程的node,当前线程即线程B
      	//arg = 1
          boolean failed = true;
          try {
              boolean interrupted = false;
              //死循环
              for (;;) {
                  //首先将当前线程的node的前一个node 赋值给p
                  //此时p就等于线程B的前一个节点,在enq方法中我们可以得出,p节点就是装载了线程A的那个Node
                  final Node p = node.predecessor();
                  //第一次判断p==head是符合的,所以继续进行判断,tryAcquire(尝试加锁)
                  /*
                   *这里说两种情况。
                   *1.如果在这里线程A运行完毕释放了锁,那么尝试加锁肯定会成功
                   *那么就将head设置为线程B的node
                   *p.next即线程A的next指针,将其指向null,这是为了让其无引用,帮助GC回收
                   *然后整个方法返回false(这里为什么返回interrupted,后文会说)
                   *
                   *2.如果这里线程A没有运行完毕,那么尝试加锁失败。那么程序不会进入这个if,继续往下执行。
                   *其实这里的本质就是:在判断自己需要排队后,不立即park,而是先自旋一次,尝试获取锁,如果获取锁成功了,则直接就可以拿到锁,而不必进行上下文切换,如果尝试获取锁失败,再做别的操作,这样的尝试获取锁会重复两次,也就是说线程在park之前会自旋两次!
                  */
                  if (p == head && tryAcquire(arg)) {
                      setHead(node);
                      p.next = null; // help GC
                  	failed = false;
                      return interrupted;
              	}
                  //如果程序尝试获取锁失败,则会进入这个判断,执行shouldParkAfterFailedAcquire方法
                  //shouldParkAfterFiledAcquire传入两个参数,即装载了线程A的Node和装载了线程B的Node
                  //看到这的同学跳到7,有shouldParkAfterFailedAcquire方法。
      /*==================================分割线==================================*/
                  //由7可知shouldParkAfterFailedAcquire方法返回false,于是进入下一次循环。
                  /*在第二次循环中,同样会再一次尝试获取锁,然后如果锁获取失败,
                   *同样会进入shouldParkAfterFailedAcquire方法
                   *然后由于第一次在该方法中已经将waitStatus修改为Node.SIGNAL了(表明线程是否已经准备好被阻塞并等待唤醒)
                   *所以shouldParkAfterFailedAcquire会返回true
                   *于是进入parkAndCheckInterrupt方法,这个方法中只有两行代码
                   *LockSupprt.park(this);//让线程进入阻塞状态
                   *return Thread.interrupted();//如果线程被打断则返回true,这个地方存在坑,后文会提到。
                   *所以线程B在两次自旋尝试加锁失败后,就会进入阻塞状态。
                   *这个方法结束,但不会正常返回,因为线程此时已经阻塞在这里了。
                   *所以最初调用的lock方法也不会得到正常返回,所以整个线程就被卡在lock方法的那一行。无法进入共享区域。
                   *这也是我刚才提到的,只要lock正常返回说明加锁成功,没有正常返回代表线程被阻塞。
                  */
                  if (shouldParkAfterFailedAcquire(p, node) &&
                  parkAndCheckInterrupt())
                      interrupted = true;
          	}
          } finally {
              if (failed)
                  cancelAcquire(node);
          }
      }
      

      7. Поток B входит в метод shouldParkAfterFailedAcquire (необходима ли парковка после неудачной попытки получить блокировку).

      private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
          /*
           *shouldParkAfterFailedAcquire(在尝试获取锁失败后是否需要park)方法有三个作用:
           *1、若pred.waitStatus状态位大于0,说明这个节点已经取消了获取锁的操作,doWhile循环会递归删除掉这些放弃获取锁的节点。
           *2、若状态位不为Node.SIGNAL,且没有取消操作,则会尝试将状态位修改为Node.SIGNAL。
           *3、若状态位是Node.SIGNAL,表明线程是否已经准备好被阻塞并等待唤醒。
           */
          
      	//该方法传入两个参数,即当前线程的node和当前node的pred
          //首先将pred的waitStatus属性赋值给ws,当前waitStatus属性为0
      	int ws = pred.waitStatus;
          //Node.SIGNAL说明该节点准备好被阻塞并等待唤醒,若节点没有设置为该状态,线程不会阻塞。当前节点的pred的waitStatus是没有被设置为该状态的
          if (ws == Node.SIGNAL)
          	return true;
          //当前ws等于0而不是大于0
          if (ws > 0) {
              do {
                  node.prev = pred = pred.prev;
              } while (pred.waitStatus > 0);
              
              pred.next = node;
          } else {
              //进入这个代码块
              //尝试CAS改变 pred!pred!pred!(重要的事情说三遍)的waitStatus属性,从0改为Node.SIGNAL
              //也就是说,后一个Node,会改变前一个Node的waitStatus属性,换言之,就是当前线程Node的waitStatus属性只能由后面的那一个节点改变,再换言之,前一个node的waitStatus标识着自己的等待状态。
          	compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
          }
          //整个方法返回false
          return false;
      }    
      
    • Процесс блокировки потока C

      1.lock

      //线程C获取锁
      public void lock() {
      	sync.lock();
      }
      

      2. Честный замок

      final void lock() {
          /*公平锁的lock()方法实际上封装的是acquire方法,而非公平锁是直接进行CAS操作尝试获取锁
            如果获取锁失败,调用acquire方法。
          */
      	acquire(1);
      }
      

      3. Метод получения

      public final void acquire(int arg) {
          /*线程B首先调用tryAcquire方法尝试获取锁,看到这的同学进入4.tryAcquire方法*/
      /*==================================分割线==================================*/
          /*tryAcquire方法返回false,取反,于是继续往下判断,执行addWaiter方法,看到这的同学跳转进入5,addWaiter方法*/
      /*==================================分割线==================================*/
          /*addWaiter方法返回保存线程C的Node,然后执行acquireQueued方法,看到这的小伙伴进入6,acquireQueued方法*/
          if (!tryAcquire(arg) &&
              acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
              selfInterrupt();
          
      }
      

      4. метод tryAcquire

      //线程C从acquire方法进入tryAcquire方法
      protected final boolean tryAcquire(int acquires) {
          //获取当前线程:线程C
          final Thread current = Thread.currentThread();
          //获取当前state状态,state是锁是否被占用的标志位,0表示自由,1表示被占用
          //当前线程A正在持有锁,所以state==1,即c==1
          int c = getState();
          if (c == 0) {
              //此时c==1,于是不能到达这里
              if (!hasQueuedPredecessors() &&
                  compareAndSetState(0, acquires)) {
                  setExclusiveOwnerThread(current);
                  return true;
              }
          }
          else if (current == getExclusiveOwnerThread()) {
              /*这个判断是用来判断线程是否重入的,如果持有锁的线程是当前线程,那么无需进行CAS获取锁,直接上锁*/
              //判断当前线程是否为持有锁的线程
              //显然线程C不是当前持有锁的线程,于是也到不了这里。
              int nextc = c + acquires;
              if (nextc < 0)
                  throw new Error("Maximum lock count exceeded");
              setState(nextc);
              return true;
          }
          //线程C整个tryAcquire方法返回false
          return false;
      }
      

      5. метод addWaiter

      private Node addWaiter(Node mode) {
          //Node中保存了当前线程,还保存了当前线程的前驱节点、后继节点,同步状态、等待状态。AQS是由Node组成的
          //创建一个Node,并将其中的thread属性改为当前线程,也就是线程C
          Node node = new Node(Thread.currentThread(), mode);
          //将tail赋值给pred,但此时tail为保存了线程B的Node
          Node pred = tail;
          if (pred != null) {
              //此时pred不等于null,队列已经被初始化,而且其中有两个Node,于是进入这个判断内
              //将pred赋值给线程C的Node的前驱指针。
              //此时pred就是保存线程B的Node
              node.prev = pred;
              //进行一次CAS操作,将tail指向保存了线程C的Node
              //此时整个AQS队列应该是这样的
              //正在持有锁的Node(Head)<——>保存线程B的Node<——>保存线程C的Node(Tail)
              if (compareAndSetTail(pred, node)) {
                  pred.next = node;
                  return node;
                  //方法返回。返回值为保存线程C的Node
              }
          }
          enq(node);
          return node;
      }
      

      6. Метод AcquireQueued

      final boolean acquireQueued(final Node node, int arg) {
      	//该方法传入一个node,即保存了当前线程的node,当前线程即线程C
      	//arg = 1
          boolean failed = true;
          try {
              boolean interrupted = false;
              //死循环
              for (;;) {
                  //首先将当前线程的node的前一个node 赋值给p,即p==保存了线程B的Node
                  final Node p = node.predecessor();
                  //p此时在队列中排第二个位置,所以它不是头部,不会进入该判断。
                  /*
                   *这里解释一下它的用意,当一个线程的前一个线程是队首,那么它在进入这个方法时,
                   *有可能队首的线程已经执行完毕并将锁释放
                   *那么可以去尝试获取一下锁
                   *但是如果你的前一个Node都不是队首,说明你的前面还有人在排队
                   *所以你就不用再去尝试获取锁了,因为还轮不到你。
                   *所以直接放弃执行这段代码,乖乖去排队吧。
                  */
                  if (p == head && tryAcquire(arg)) {
                      setHead(node);
                      p.next = null; // help GC
                      failed = false;
                      return interrupted;
                  }
                  //于是进入这个判断,直接判断是否需要park
                  //流程和刚才线程B执行shouldParkAfterFailedAcquire方法是一样的
                  //所以我就不重新贴代码了。
                  //最终线程C会在这里被park住
                  //方法结束,但是不会正常返回,线程C就直接阻塞在这了,等待前面的线程执行完来唤醒他吧。
                  if (shouldParkAfterFailedAcquire(p, node) &&
                      parkAndCheckInterrupt())
                      interrupted = true;
              }
          } finally {
              if (failed)
                  cancelAcquire(node);
          }
      }
      
      
  • Краткое описание процесса блокировки

    • Первый поток получает блокировку

      Когда блокировка простаивает, первый поток пытается получить блокировку, что является ситуацией, в которой поток A получает блокировку, описанную выше. В это время очередь также не инициализирована, фактически после того, как поток А успешно получит блокировку, очередь также не будет инициализирована. Можно видеть, что весь процесс получения блокировки потоком А проходит очень гладко, не используются системные вызовы, не выполняется преобразование пользовательского режима в режим ядра, и все операции полностью выполняются на Java. Итак, если нитьАльтернативное исполнение, то очень эффективно использовать ReentrantLock как средство синхронизации. Поскольку нет блокировки потока, нет необходимости делать системный вызов для блокировки.

    • Второй поток получает блокировку

      Когда блокировка уже занята, второй поток пытается получить блокировку, что может соответствовать сценарию, в котором поток B получает блокировку выше. На данный момент очередь не инициализирована, поэтому поток B перейдет кИнициализировать всю очередь, но в это время в очереди находится не только поток B, но и узел перед потоком B. Хотя атрибут потока этого узла равен нулю, мы можем рассматривать его как то, что он представляетпоток A в настоящее время держит блокировку. Поскольку поток A не участвовал в постановке в очередь от начала до конца, свойство потока первого Node будет равно null; или можно понимать так:В очереди первый человек, стоящий в очереди, не стоит в очереди, а занимается бизнесом, и тогда второй человек или более поздние могут рассматриваться как стоящие в очереди.. Сказав так много, я просто хочу показать, что сейчас в очереди два узла. Затем, когда B завершит создание узла, поскольку поток B является вторым в очереди, его очередь наступит после выполнения A, поэтому в это время поток B проверит, завершил ли поток A выполнение и снял блокировку. получить замок, которыйпервый спин. еслиПопытка получить замок не удалось, на этом все закончилось, вот-вот будет парковаться, так что давайте подчищаем и меняем waitStatus предыдущего узла на Node.SIGNAL, что означает, что он готов к блокировке, но перед паркингом поток B перезапустится поставит умирающая борьба,Что если! Что, если замок можно приобрести сейчас? ! ! !, поэтому он снова пытается получить блокировку, и если это снова не удается, он напрямую блокируется парком.

    • Третий поток получает блокировку

      Когда блокировка занята и очередь инициализирована, перед очередью также стоят потоки. Это ситуация, с которой столкнулся поток C. В это время поток C также попытается получить блокировку, он должен быть не в состоянии получить ее, потому что впереди все еще есть очереди, и сейчас не ваша очередь. Так что он сразу стал ждать в очереди. Ожидая в очереди, он смотрел на свой статус. В-третьих, не пытайтесь получить замок. Перед ним все еще куча людей. припарковался на месте.но, третье место не означает потерю надежды! Он дважды прокрутится, чтобы увидеть, есть ли у него шанс перейти на вторую позицию, и если он находится во второй позиции, он все равно попытается получить блокировку.

    • Окончательная диаграмма очереди

      AQS最终队列示意图

    • Суть замка

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

  • Яма осталась в процессе блокировки

    • Почему метод parkAndCheckInterrupt() возвращает Thread.interrupted() вместо прямого возврата true или false или не возвращает его напрямую?

      Сначала перейдите к исходному коду parkAndCheckInterrupt:

      private final boolean parkAndCheckInterrupt() {
          LockSupport.park(this);//调用park()使线程进入waiting状态
      	return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
      }
      

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

      Тогда возникает другой вопрос, поток, который получает блокировку через метод lock(), если блокировка занята и поток заблокирован, если вызывается метод interrupt() заблокированного потока, будет ли отменено получение блокировки ? ответотрицательный. LockSupport.park будет реагировать на прерывание, но не будет генерировать InterruptedException() (в это время можно вызвать метод lockInterruptably() для блокировки, если поток будет прерван, будет брошено InterruptedException()). То есть, если мы используем метод блокировки для блокировки, а затем используем прерывание для прерывания потока, он не получит никакого ответа, так зачем возвращать статус прерывания в parkAndCheckInterrupt? Плохо ли возвращать void напрямую?Давайте посмотрим на логику метода lockInterruptably().:

      public void lockInterruptibly() throws InterruptedException {
      	sync.acquireInterruptibly(1);
      }
      
      public final void acquireInterruptibly(int arg) throws InterruptedException {
      	if (Thread.interrupted())
              //如果线程已经中断,则直接抛出异常
      		throw new InterruptedException();
      
      	if (!tryAcquire(arg))
      		doAcquireInterruptibly(arg);
      }
      
      private void doAcquireInterruptibly(int arg) throws InterruptedException {
      	final Node node = addWaiter(Node.EXCLUSIVE);
          boolean failed = true;
          try {
          	for (;;) {
              final Node p = node.predecessor();
              if (p == head && tryAcquire(arg)) {
              	setHead(node);
                  p.next = null; // help GC
                  failed = false;
                  return;
              }
              if (shouldParkAfterFailedAcquire(p, node) &&
              	parkAndCheckInterrupt())
                  //这里和普通的lock加锁不一样了。
                  //lock加锁只是将interrupted状态记录下来
                  //interrupted=true;
                  //而这里会直接抛出异常
                  throw new InterruptedException();
              }
      	} finally {
      		if (failed)
      			cancelAcquire(node);
      	}
      }
      

      Итак, почему метод parkAndCheckInterrupt() возвращает Thread.interrupted() вместо void? На самом деле, это потому, что parkAndCheckInterrupt также нужно вызывать в doAcquireInterruptently, чтобы логику кода в этом методе можно было повторно использовать в блокировке блокировки и lockInterruptably, чтобы возвращалось состояние прерванного.

      Оставьте мысль,selfInterruptКакова роль(), зачем нужно снова вызывать этот метод и какие будут последствия, если вы его не вызовете? Читатели приглашаются к обсуждению в разделе комментариев.

  • Небольшой разговор о Synchronized и ReentrantLock

    До Java 1.6 Synchronized был бескомпромиссной тяжеловесной блокировкой.Каждый процесс блокировки требовал переключения между режимом пользователя и режимом ядра и блокировал потоки, которые не получали блокировку.Запущенный в то время ReentrantLock выжил, чтобы решить эту проблему. Но теперь Synchronized провел большую оптимизацию после Java 1.6, представил Spin, Adaptive Spin и разделил Synchronzied на четыре состояния: без блокировки, предвзятая блокировка, облегченная блокировка, усиленная блокировка, обновление блокировки и понижение блокировки автоматически выполняются в разных сценариях. , так что теперь Synchronized может избавиться от своей блокировки веса, и даже его эффективность может превзойти ReentrantLock в некоторых сценариях.ReentrantLock все еще необходим?, очевидно, да, ReentrantLock предоставляет богатый API, он более гибкий в использовании и обеспечивает реализацию справедливых и нечестных блокировок, чего не может Synchronized.

Видимость памяти JMM

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

    Модель памяти Java сама по себе является абстрактным понятием и на самом деле не существует.Она описывает набор правил или спецификаций, посредством которых различные переменные в программе (включая поля экземпляра, статические поля и элементы, составляющие объект массива).Это не совсем то же самое, что и концепция модели памяти Java JVM. Вы можете посмотреть мой предыдущий блог. Модель памяти JVM Java написана на ней. Из-за того же имени, но разных концепций концепции модели памяти в этой статье упоминаются вместе в качестве JMM Модель памяти Java JVM также называется моделью памяти Java., JMM и разделение памяти Java - это разные концептуальные уровни. JMM описывает набор правил, которые вращаются вокруг атомарности, видимости и упорядоченности. Единственное сходство между ним и моделью памяти Java заключается в том, чтоКак общие, так и частные зоны, общая область JMM — это основная память, в то время как общая область модели памяти Java включает в себя область кучи и область методов; частная область JMM — это рабочая память, а частная область модели памяти Java включает счетчик программ, стек виртуальной машины Java, стек локальных методов.

    JMM

    JVM создаст рабочую память (пространство стека) для каждого потока для хранения личных данных потока, в то время как Java предусматривает, что все переменные существуют в основной памяти, основная память является общей областью памяти, к которой могут обращаться все потоки, но поток Операции над переменными должны выполняться в рабочей памяти. Таким образом, каждый поток сначала копирует переменную из основной памяти в рабочую память, а затем записывает переменную обратно в основную память после завершения операции.

  • оперативная память JMM

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

  • оперативная память JMM

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

  • Кратко описаны типы хранения данных в основной и рабочей памяти, а также методы работы с ними.

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

    2.тип ссылкилокальные переменные, ссылочные объекты хранятся во фрейме стека, а объекты-экземпляры хранятся в основной памяти.

    3.переменные-члены экземпляра объекта,статическая переменная,информация о классехранятся в основной памяти.

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

  • Как JMM решает проблему видимости памяти

    • Условия, которые необходимо выполнить для переупорядочивания инструкций

      1. Результат выполнения программы нельзя изменить в однопоточной среде. 2. Изменение порядка не допускается, если есть зависимости данных. То есть переупорядочение инструкций может быть выполнено только в том случае, если оно не может быть выведено по принципу «происходит до».

    • происходит до принципа

      Результат операции A должен быть виден операции B, тогда между A и B возникает связь «происходит до».,Например:

      i=1;//线程A执行
      j=i;//线程B执行
      /*由于B依赖A执行的结果,所以线程A happens-before B*/
      

      Восемь принципов того, что происходит раньше (я тоже выглядел сбитым с толку):

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

      2. Правила блокировки: сначала выполняется операция разблокировки перед последующей операцией блокировки для той же блокировки.

      3. Правило volatile-переменной: операция записи в переменную происходит раньше, чем последующая операция чтения переменной.

      4. Правило доставки: если операция A происходит первой в операции B, а операция B происходит первой в операции C, то можно сделать вывод, что операция A происходит первой в операции C.

      5. Правила запуска потока: метод start() объекта Thread выполняется первым для каждого действия этого потока.

      6. Правила прерывания потока: вызов метода прерывания потока () происходит первым, когда код терминального потока обнаруживает возникновение события прерывания.

      7. Правила завершения потока: все операции в потоке происходят первыми при обнаружении завершения потока.Мы можем завершить его с помощью метода Thread.join(), а возвращаемое значение Thread.isAlive() определяет, что поток завершил выполнение.

      8. Правила финализации объекта. Инициализация объекта происходит сначала в начале его метода finalize().

      Если две операции не удовлетворяют ни одному из приведенных выше правил «происходит до», то порядок этих двух операций не гарантируется, и JVM может переупорядочить эти две операции;Если операция А происходит раньше операции Б, то операция А совершается над операцией памяти Б..

    • Видимость изменчивого

      volatile: это облегченный механизм синхронизации, предоставляемый JVM. он можетУбедитесь, что оформленная общая переменная всегда видна всем потокам.,Достаточно хорошоОтключить переупорядочивание инструкций, но Volatile в многопоточной средеБезопасность не гарантируется.

      public class VolatileTest{
      	public static volatile int value = 0;
      	public static void main(String[] args){
      		inc();
      	}
      	public static void inc(){
      		value++;
      	}
      }
      

      Значение изменяемого ключевого слова volatile будет обновляться в основной памяти сразу же при каждом его выполнении, но в многопоточной среде возникнут проблемы с безопасностью, поскольку операция value++ не является атомарной, вы можете использовать синхронизированную модификацию метод обеспечения его безопасности.

    • Почему volatile-модифицированные переменные могут гарантировать видимость?

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

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

    • Как volatile предотвращает изменение порядка

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

    • Разница между volatile и синхронизированным

      1. Суть volatile в том, чтобы сообщить JVM, что значение текущей переменной в регистре (рабочей памяти) неопределенно и его нужно считать из памяти, synchronized — в блокировке текущей переменной, только текущий поток может получить доступ к переменная, а другие потоки блокируются до тех пор, пока поток не завершит операцию с переменной.

      2.volatile можно использовать только на уровне переменной, а synchronized можно использовать на уровне метода, переменной и класса.

      3. volatile может обеспечить только видимость модификации переменных и не гарантирует атомарность. А синхронизация может гарантировать видимость и атомарность модификации переменных.

      4.volatile не приведет к блокировке потока, синхронизация может привести к блокировке потока.

      5. Переменные, помеченные как volatile, не будут оптимизированы компилятором, а переменные, помеченные как synchronized, могут быть оптимизированы компилятором.

CAS

  • CAS (CompareAndSwap) — эффективный способ обеспечения безопасности потоков.

    1. Он поддерживает операции атомарного обновления, подходящие для счетчиков, секвенсоров и других сценариев.

    2. Принадлежатоптимистическая блокировкамеханизм.

    3. В случае сбоя операции CAS разработчик решает, продолжать ли попытки или выполнить другие операции.

  • CAS думал

    CAS содержит три операнда, а именноV (ячейка памяти), E (старое значение), N (новое значение), если и только еслиV==Eвремя, можетИзменить V на N, в противном случае разработчик должен повторить попытку или выполнить другие операции.Фактически, серия классов Atomic в JUC использует CAS для обеспечения атомарной работы переменных.. Во многих случаях разработчикам не нужно напрямую использовать CAS для решения проблем безопасности потоков, а нужно напрямую использовать пакет JUC для обеспечения безопасности потоков.

  • Недостатки CAS

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

    2. Гарантируется только атомарная операция общей переменной.

    3.АВА-проблема, Предположим, что поток считывает значение переменной A, и значение переменной по-прежнему равно A, когда оно готово к назначению. Можно ли гарантировать, что значение этой переменной не было изменено другими потоками? Что, если значение этой переменной сначала изменится на B другим потоком, а затем изменится на A другим потоком во время изменения значения?решить: AtomicStampedReference, представляет номер версии.

оптимистическая блокировка и пессимистическая блокировка

Наконец, давайте поговорим об относительно расслабленном содержании, оптимистичной блокировке и пессимистичной блокировке, давайте представим концепцию.

  • оптимистическая блокировка

    Всегда предполагайте наилучшую ситуацию.Каждый раз, когда я иду за данными, я думаю, что другие не изменят их, поэтому они не будут заблокированы.Однако при обновлении он будет судить, обновляли ли другие данные в течение этого периода.Вы можно использоватьМеханизм номера версии и алгоритм CASвыполнить. Оптимистическая блокировка подходит для типов приложений с множественным чтением, что может повысить пропускную способность.Как и механизм write_condition, предоставляемый базой данных, на самом деле это оптимистическая блокировка. Класс атомарной переменной в пакете java.util.concurrent.atomic в Java — это реализация, использующая оптимистическую блокировку ——CASосуществленный.

  • пессимистический замок

    Всегда предполагайте наихудший случай, каждый раз, когда вы идете за данными, вы думаете, что другие изменят их, поэтому каждый раз, когда вы получаете данные, вы блокируете их, чтобы другие, которые хотят получить данные, блокировались до тех пор, пока они не будут получены. блокировка ** (общие ресурсы используются только одним потоком за раз, другие потоки блокируются, а ресурсы передаются другим потокам после использования) **. Многие такие механизмы блокировки используются в традиционных реляционных базах данных, например блокировки строк, таблиц, чтения, записи и т. д., все из которых блокируются перед выполнением операций. Эксклюзивные блокировки, такие как synchronized и ReentrantLock в Java, являются реализацией идеи пессимистической блокировки.

  • Два сценария использования блокировки

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

Эпилог

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

Название этого "Замок, в качестве резюме того, что я узнал о параллельном программировании за этот период времени.

Наконец, я желаю всем счастливого национального праздника и хорошо провести время.

Фотографии в этой статье взяты из Интернета, захвачены и удалены.

Добро пожаловать в мой личный блог:Object's Blog