Принцип синхронного выполнения

Java JVM Операционная система Безопасность

0. Предисловие

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

1. Три метода применения синхронизированного

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

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

1.1 синхронизированные действия над методами экземпляра

Так называемая блокировка объекта экземпляра предназначена для использования synchronized для изменения метода экземпляра в объекте экземпляра,Обратите внимание, что методы экземпляра не включают статические методы.

public class AccountingSync implements Runnable{
    //共享资源(临界资源)
    static int i=0;

    /**
     * synchronized 修饰实例方法
     */
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<100;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AccountingSync instance=new AccountingSync();
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        System.out.println(i);//200
    }
    
}

В приведенном выше коде мы запускаем два потока для работы с одним и тем же общим ресурсом, то есть с переменной i. Поскольку операция i++ не является атомарной, операция заключается в том, чтобы сначала прочитать значение, а затем записать новое значение, что эквивалентно добавлению 1 к исходному значению. , выполняемое в два этапа. Если второй поток считывает значение i, в то время как первый поток читает старое значение и записывает обратно новое значение, то второй поток видит то же значение, что и первый поток, и выполняет операцию добавления 1 того же значения, что также обеспечивает безопасность потоков. отказ. Следовательно, для метода увеличения необходимо использовать синхронизированную модификацию, чтобы обеспечить потокобезопасность. На этом этапе мы должны заметить, что модификатор synchronized — это метод экземпляра, в данном случае блокировка текущего потока — это объект экземпляра.Обратите внимание, что блокировки синхронизации потоков в Java могут быть произвольными объектами.. Судя по результату выполнения кода, это действительно правильно, если не использовать ключевое слово synchronized, то конечный результат вывода, скорее всего, будет меньше 200. В этом роль ключевого слова synchronized. Здесь также нужно понимать, что когда поток обращается к синхронизированному методу экземпляра объекта, другие потоки не могут получить доступ к другим синхронизированным методам объекта, ведь объект имеет только одну блокировку.Когда поток получает блокировку объекта , Другие потоки не могут получить блокировку объекта, поэтому они не могут получить доступ к другим синхронизированным методам экземпляра объекта, но другие потоки могут получить доступ к другим несинхронизированным методам экземпляра объекта. Конечно, если потоку A необходимо получить доступ к синхронизированному методу f1 экземпляра объекта obj1 (текущая блокировка объекта — obj1), а другому потоку B необходимо получить доступ к синхронизированному методу f2 экземпляра объекта obj2 (текущая блокировка объекта — obj2), это разрешено, т.к. блокировки двух объектов-экземпляров не совпадают. В это время, если два потока работают с данными, которые не являются общими, безопасность потока гарантируется. К сожалению, если два потока работают с общие данные, безопасность потока может не гарантироваться. , следующий код продемонстрирует это явление:

public class AccountingSyncBad implements Runnable{
    static int i=0;
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncBad());
        //new新实例
        Thread t2=new Thread(new AccountingSyncBad());
        t1.start();
        t2.start();
        System.out.println(i);//146
    }
}

Отличие приведенного выше кода от предыдущего заключается в том, что мы одновременно создаем два новых экземпляра AccountingSyncBad, а затем запускаем два разных потока для работы с общей переменной i, но, к сожалению, результатом операции является 146 вместо ожидаемый результат 200, потому что приведенный выше код допустил серьезную ошибку Ошибка, хотя мы используем синхронизированный для изменения метода увеличения, но новые два разных объекта экземпляра, что означает, что есть две разные блокировки объекта экземпляра, поэтому t1 и t2 будут введите свои соответствующие блокировки объектов, а также То есть потоки t1 и t2 используют разные блокировки, поэтому безопасность потока не может быть гарантирована. Способ решения этой дилеммы заключается в применении синхронизированного к статическому методу увеличения, в этом случае блокировка объекта является текущим объектом класса, потому что независимо от того, сколько объектов экземпляра создается, существует только один объект класса, все в этом случае Блокировки объекта уникальны. Давайте посмотрим, как использовать синхронизированный со статическим методом увеличения.

1.2 синхронизированные действия над статическими методами

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

public class AccountingSyncClass implements Runnable{
    static int i=0;

    /**
     * 作用于静态方法,锁是当前class对象,也就是
     * AccountingSyncClass类对应的class对象
     */
    public static synchronized void increase(){
        i++;
    }

    /**
     * 非静态,访问时锁不一样不会发生互斥
     */
    public synchronized void increase4Obj(){
        i++;
    }

    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncClass());
        //new新实例
        Thread t2=new Thread(new AccountingSyncClass());
        //启动线程
        t1.start();
        t2.start();
        System.out.println(i);
    }
}

Поскольку ключевое слово synchronized изменяет метод статического увеличения, в отличие от метода модифицированного экземпляра, объект блокировки является объектом класса текущего класса. Обратите внимание, что в коде метод увеличения4Obj является методом экземпляра, а его объектная блокировка — это текущий объект экземпляра.Если другие потоки вызовут этот метод, взаимного исключения не будет.В конце концов, объекты блокировки разные, но мы должны осознавать что в этом случае может быть обнаружена проблема безопасности потока (управляемая общая статическая переменная i).

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

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

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    @Override
    public void run() {
        //省略其他耗时操作....
        //使用同步代码块对变量i进行同步操作,锁对象为instance
        synchronized(instance){
            for(int j=0;j<1000000;j++){
                    i++;
              }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        System.out.println(i);
    }
}

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

//this,当前实例对象锁
synchronized(this){
    for(int j=0;j<100;j++){
        i++;
    }
}

//class对象锁
synchronized(AccountingSync.class){
    for(int j=0;j<100;j++){
        i++;
    }
}

3. Лежащая в основе семантика синхронизированного

Синхронизация в виртуальной машине Java реализована на основе входа и выхода из объектов монитора, будь то явная синхронизация (с явными инструкциями monitorenter и monitorexit, то есть блоками кода синхронизации) или неявная синхронизация. В языке Java наиболее распространенным местом для синхронизации, вероятно, является метод synchronized, модифицированный с помощью synchronized. Метод синхронизации не синхронизируется инструкциями monitorenter и monitorexit, а неявно реализуется инструкцией вызова метода, читающей флаг ACC_SYNCHRONIZED метода в пуле констант времени выполнения.Этот момент будет подробно проанализирован позже. Давайте сначала разберемся с концепцией заголовка объекта Java, которая очень важна для глубокого понимания принципа реализации синхронизированного.

3.1 Понимание заголовка и монитора объекта Java

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

对象存储布局

  • Данные экземпляра: храните информацию об атрибутах класса, включая информацию об атрибутах родительского класса.
  • Заполнение выравнивания: поскольку виртуальная машина требует, чтобы начальный адрес объекта был целым числом, кратным 8 байтам. Данные заполнения не требуются, они нужны только для выравнивания байтов.
  • Заголовок объекта: заголовок объекта Java обычно занимает 2 машинных кода (в 32-битной виртуальной машине 1 машинный код равен 4 байтам, то есть 32 бита, в 64-битной виртуальной машине 1 машинный код равен 8 байтам, т.е. 64-битный), но если объект является типом массива, требуются 3 машинных кода, поскольку виртуальная машина JVM может определить размер объекта Java с помощью метаданных объекта Java, но не может подтвердить размер массива. из метаданных массива, поэтому используйте блок для записи длины массива.

Чтобы представить свойства, методы и другую информацию об объекте, необходимо описание структуры. Hotspot VM использует указатель в заголовке объекта, чтобы указать на область класса, чтобы найти описание класса объекта, а также записи внутренних методов и свойств. Как показано ниже:

биты виртуальной машины структура объекта заголовка иллюстрировать
32/64bit Mark Word Хранит данные времени выполнения самого объекта, такие как хэш-код (HashCode), возраст генерации GC, флаги состояния блокировки, блокировки, удерживаемые потоками, смещенный идентификатор потока, смещенную отметку времени, возраст создания объекта и т. д.; Mark Word предназначен для Нефиксированная структура данных для хранения как можно большего количества информации в очень небольшом пространстве, она будет повторно использовать собственное пространство для хранения в соответствии со своим состоянием.
32/64bi Class Metadata Address Указатель типа указывает на метаданные класса объекта, и JVM использует этот указатель, чтобы определить, экземпляром какого класса является объект.
32/64bi Array Length Если объект представляет собой массив Java, в заголовке объекта также должна быть часть данных, в которой записана длина массива. Поскольку виртуальная машина может определить размер объекта Java с помощью информации метаданных обычного объекта Java, но размер массива не может быть определен из метаданных массива. Длина этой части данных составляет 32 бита и 64 бита соответственно в 32-битных и 64-битных виртуальных машинах (без открытия сжатого указателя).

Например, в 32-битной виртуальной машине HotSpot, если объект находится в разблокированном состоянии, 25 бит 32-битного пространства в Mark Word используется для хранения хэш-кода объекта, 4 бита используются для хранения генерации объекта. возраст и 2 бита используются для хранения возраста создания объекта.Бит флага блокировки хранилища, 1 бит фиксируется на 0, как показано в следующей таблице:

无锁状态
Поскольку информация заголовка объекта требует дополнительных затрат на хранение и не имеет ничего общего с данными, определенными самим объектом, с учетом эффективности использования пространства JVM, Mark Word разработан как нефиксированная структура данных для хранения более эффективные данные.Состояние самого объекта повторно использует свое собственное пространство для хранения.Например, под 32-битной JVM, в дополнение к структуре хранения Mark Word по умолчанию, указанной выше, могут измениться следующие структуры:

В 64-битной виртуальной машине размер Mark Word составляет 64 бита, а структура его хранения выглядит следующим образом:

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

Среди них облегченные блокировки и предвзятые блокировки были недавно добавлены после оптимизации синхронизированных блокировок в Java 6. Мы кратко проанализируем их позже. Здесь мы в основном анализируем тяжеловесную блокировку, которую обычно называют блокировкой синхронизированного объекта.Бит идентификации блокировки равен 10, а указатель указывает на начальный адрес объекта-монитора (также известного как блокировка монитора или монитора). С каждым объектом связан монитор, и связь между объектом и его монитором может быть реализована различными способами, например, монитор может создаваться и уничтожаться вместе с объектом или автоматически генерироваться, когда поток пытается получить объект. lock, но когда монитор однажды удерживается потоком, он блокируется. В виртуальной машине Java (HotSpot) монитор реализован ObjectMonitor, а его основная структура данных выглядит следующим образом (находится в файле ObjectMonitor.hpp исходного кода виртуальной машины HotSpot, реализованного на C++)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

В ObjectMonitor есть две очереди, _WaitSet и _EntryList, которые используются для сохранения списка объектов ObjectWaiter (каждый поток, ожидающий блокировки, будет инкапсулирован в объект ObjectWaiter), _owner указывает на поток, содержащий объект ObjectMonitor, когда несколько потоков доступ к синхронизации в то же время Когда код выполняется, он сначала войдет в коллекцию _EntryList.Когда поток получает монитор объекта, он входит в область _Owner и устанавливает переменную владельца в мониторе для текущего потока. в то же время счетчик счетчика в мониторе увеличивается на 1. Если поток вызывает метод wait(), текущий удерживаемый монитор будет освобожден, переменная владельца будет восстановлена ​​до нуля, счетчик будет уменьшен на 1, и поток войдет в коллекцию WaitSet и будет ждать пробуждения. Если текущий поток завершает выполнение, он также освобождает монитор (блокировка) и сбрасывает значение переменной, чтобы другие потоки могли войти и получить монитор (блокировка). Как показано ниже

С этой точки зрения объект монитора существует в заголовке объекта каждого объекта Java (указывающего на сохраненный указатель), и синхронизированная блокировка получает блокировку таким образом, поэтому любой объект в Java может использоваться в качестве блокировки. , и в то же время Это также является причиной того, что в объекте верхнего уровня Object существуют такие методы, как notify/notifyAll/wait (этот момент будет проанализирован позже). реализация синхронизации на уровне байт-кода.

3.2 Основной принцип синхронизированного кодового блока

Теперь мы переопределяем синхронизированный модифицированный синхронизированный блок кода для работы с общей переменной i в блоке кода следующим образом.

public class SyncCodeBlock {

   public int i;

   public void syncTask(){
       //同步代码块
       synchronized (this){
           i++;
       }
   }
}

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

Classfile /***/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
  Last modified 2018-07-25; size 426 bytes
  MD5 checksum c80bc322c87b312de760942820b4fed5
  Compiled from "SyncCodeBlock.java"
public class com.hc.concurrencys.SyncCodeBlock
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
  //........省略常量池中数据
  //构造函数
  public com.hc.concurrencys.SyncCodeBlock();
    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 7: 0
  //===========主要看看syncTask方法实现================
  public void syncTask();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter  //注意此处,进入同步方法
         4: aload_0
         5: dup
         6: getfield      #2             // Field i:I
         9: iconst_1
        10: iadd
        11: putfield      #2            // Field i:I
        14: aload_1
        15: monitorexit   //注意此处,退出同步方法
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit //注意此处,退出同步方法
        22: aload_2
        23: athrow
        24: return
      Exception table:
      //省略其他字节码.......
}
SourceFile: "SyncCodeBlock.java"

В основном мы фокусируемся на следующем коде в байт-коде

3: monitorenter  //进入同步方法
//..........省略其他  
15: monitorexit   //退出同步方法
16: goto          24
//省略其他.......
21: monitorexit //退出同步方法

Из байт-кода видно, что реализация блока операторов синхронизации использует инструкции monitorenter и monitorexit, где инструкция monitorenter указывает на начальную позицию блока кода синхронизации, а инструкция monitorexit указывает на конечную позицию блока кода синхронизации. Когда выполняется инструкция monitorenter, текущий поток попытается получить право собственности на монитор, соответствующий объектной ссылке (т. е. объектная блокировка), когда счетчик входов монитора объектной ссылки равен 0, поток может успешно получить монитор и установить значение счетчика на 1, и блокировка успешно получена. Если текущий поток уже владеет монитором объектной ссылки, он может повторно войти в монитор (повторный вход будет проанализирован позже), и значение счетчика будет увеличено на 1 во время повторного входа. Если другие потоки уже владеют монитором objectref, текущий поток будет заблокирован до тех пор, пока не будет выполнен исполняющий поток, то есть будет выполнена инструкция monitorexit, исполняющий поток освободит монитор (заблокирует) и установит значение счетчика. на 0, и другие потоки будут иметь возможность удерживать монитор. Стоит отметить, что компилятор гарантирует, что независимо от того, как завершится метод, каждая инструкция monitorenter, вызываемая в методе, выполнит соответствующую инструкцию monitorexit, независимо от того, нормально или аварийно завершился метод. Чтобы убедиться, что инструкции monitorenter и monitorexit по-прежнему могут быть правильно соединены и выполнены при аварийном завершении метода, компилятор автоматически сгенерирует обработчик исключений, который объявляет, что он может обрабатывать все исключения, и его цель - выполнить команду monitorexit. инструкция. Из байт-кода также видно, что имеется дополнительная инструкция выхода монитора, которая представляет собой инструкцию по освобождению монитора, которая выполняется в конце аномалии.

3.3 Основополагающий принцип синхронного метода

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

public class SyncMethod {

   public int i;

   public synchronized void syncTask(){
           i++;
   }
}

Байт-код, декомпилированный с помощью javap, выглядит следующим образом:

Classfile /***/src/main/java/com/zejian/concurrencys/SyncMethod.class
  Last modified 2017-6-2; size 308 bytes
  MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
  Compiled from "SyncMethod.java"
public class com.hc.concurrencys.SyncMethod
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool;

   //省略没必要的字节码
  //==================syncTask方法======================
  public synchronized void syncTask();
    descriptor: ()V
    //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
}
SourceFile: "SyncMethod.java"

Из байт-кода видно, что синхронизированный модифицированный метод не имеет инструкции monitorenter и инструкции monitorexit. Вместо этого действительно есть флаг ACC_SYNCHRONIZED, который указывает, что метод является синхронизированным методом. JVM использует флаг доступа ACC_SYNCHRONIZED для идентификации a Объявлен ли метод как синхронный, чтобы выполнялся соответствующий синхронный вызов. Это основной принцип синхронизированных блокировок, реализованных на синхронизированных блоках кода и синхронизированных методах. В то же время, мы должны также отметить, что в ранней версии Java, синхронизированный является тяжеловесной блокировкой, которая неэффективна, потому что блокировка монитора (монитор) реализована с опорой на Mutex Lock (блокировка взаимного исключения) нижележащего операционная система, и Когда операционная система переключается между потоками, ей необходимо переключиться из состояния пользователя в состояние ядра.Переход между этим состоянием занимает относительно много времени, а затраты времени относительно высоки, поэтому ранняя синхронизация неэффективна . К счастью, после Java 6, Java официально оптимизировала синхронизацию с уровня JVM, поэтому эффективность синхронизированных блокировок была очень хорошо оптимизирована. блокировки и предвзятые блокировки Далее мы кратко рассмотрим официальную оптимизацию синхронизированных блокировок Java на уровне JVM.

4. Оптимизация синхронизированных виртуальных машин Java

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

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

Предвзятая блокировка — это новая блокировка, добавленная после Java 6. Это метод оптимизации операций блокировки.После исследований было обнаружено, что в большинстве случаев блокировки не только не имеют многопоточной конкуренции, но и всегда получаются одним и тем же потоком несколько раз, поэтому для снижения стоимости получения блокировок одним и тем же потоком (который включает в себя некоторые операции CAS, занимающие много времени) вводятся смещенные блокировки. Основная идея предвзятой блокировки заключается в том, что если поток получает блокировку, блокировка переходит в режим смещения, и структура слова метки также становится структурой смещения блокировки.Когда поток снова запрашивает блокировку, нет необходимо выполнить какую-либо операцию синхронизации, то есть процесс получения блокировки экономит много операций, связанных с применением блокировки, тем самым повышая производительность программы. Таким образом, в случае отсутствия конкуренции замков смещенная блокировка имеет хороший эффект оптимизации, ведь очень вероятно, что один и тот же поток применяется для одной и той же блокировки много раз подряд. Однако в случае жесткой конкуренции замков смещенная блокировка будет недействительна, так как велика вероятность того, что потоки, претендующие на блокировку, каждый раз разные, поэтому смещенную блокировку в данном случае использовать не следует, иначе выигрыш будет перевешивать потери Дело в том, что после того, как смещенная блокировка выйдет из строя, она не будет немедленно расширена до тяжеловесной блокировки, а сначала будет модернизирована до облегченной блокировки. Далее, давайте узнаем об облегченных замках.

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

Если смещенная блокировка не работает, виртуальная машина не будет немедленно обновлена ​​до тяжелой блокировки, она также попытается использовать метод оптимизации, называемый облегченной блокировкой (добавлен после версии 1.6), и структура Mark Word также станет легкой. блокировки величины. Основа того, что облегченные блокировки могут улучшить производительность программы, заключается в том, что «для большинства блокировок нет конкуренции в течение всего цикла синхронизации», обратите внимание, что это эмпирические данные. Следует понимать, что облегченная блокировка подходит для ситуации, когда потоки поочередно выполняют синхронизированные блоки. -весовой замок.

4.3 Спин-блокировки

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

4.4 Адаптивные спин-блокировки

В DK 1.6 представлена ​​более умная спин-блокировка, адаптивная спин-блокировка. Так называемая самоадаптация означает, что количество вращений больше не фиксировано, оно определяется предыдущим временем вращения на том же замке и состоянием владельца замка.

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

4.5 Устранение блокировки

Устранение блокировок — это еще один вид оптимизации блокировок для виртуальных машин.Эта оптимизация более тщательная.Виртуальные машины Java компилируются при JIT-компиляции (что можно просто понимать как компиляцию, когда определенный фрагмент кода должен быть выполнен в первый раз). , также известная как своевременная компиляция), посредством сканирования работающего контекста удаляются блокировки, которые вряд ли будут иметь конкуренцию за общие ресурсы, и таким образом устраняются ненужные блокировки, что может сэкономить время бессмысленной блокировки Метод append StringBuffer является методом синхронизации, но в методе add StringBuffer является локальной переменной и не будет использоваться другими потоками, поэтому для StringBuffer невозможна ситуация конкуренции за общие ресурсы, а JVM автоматически снимет блокировку.

public class StringBufferRemoveSync {

    public void add(String str1, String str2) {
        //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
        //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }

    public static void main(String[] args) {
        StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
        for (int i = 0; i < 100; i++) {
            rmsync.add("abc", "123");
        }
    }

}

4.6 Переходы состояний между смещенными блокировками, облегченными блокировками и тяжелыми блокировками

5. Ключевые моменты, которые вам может понадобиться знать о синхронизированных

5.1 Реентерабельность синхронизированного

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

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    static int j=0;
    @Override
    public void run() {
        for(int j=0;j<100;j++){

            //this,当前实例对象锁
            synchronized(this){
                i++;
                increase();//synchronized的可重入性
            }
        }
    }

    public synchronized void increase(){
        j++;
    }


    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t1.join();
        t2.start();
        t2.join();
        System.out.println(i);
    }
}

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

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


    public class Father  
    {  
        public synchronized void doSomething(){  
            // do something...  
        }  
    }  
      
    public class Child extends Father  
    {  
        public synchronized void doSomething(){  
            // do something...  
            super.doSomething();  
        }  
    }  

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

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

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

5.2 Прерывание потока и синхронизация

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

//中断线程(实例方法)
public void Thread.interrupt();

//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();

//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();

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

public class InterruputSleepThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                //while在try中,通过异常中断就可以退出run循环
                try {
                    while (true) {
                        //当前线程处于阻塞状态,异常必须捕捉处理,无法往外抛出
                        TimeUnit.SECONDS.sleep(2);
                    }
                } catch (InterruptedException e) {
                    System.out.println("Interruted When Sleep");
                    boolean interrupt = this.isInterrupted();
                    //中断状态被复位
                    System.out.println("interrupt:"+interrupt);
                }
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        //中断处于阻塞状态的线程
        t1.interrupt();

        /**
         * 输出结果:
           Interruted When Sleep
           interrupt:false
         */
    }
}

Как показано в приведенном выше коде, мы создаем поток и вызываем метод сна в потоке, чтобы использовать поток для входа в заблокированное состояние.После запуска потока вызовите метод прерывания объекта экземпляра потока, чтобы прервать блокирующее исключение и выдать исключение InterruptedException.Состояние также будет сброшено. Некоторые здесь могут удивиться, почему бы не использовать Thread.sleep(2000); вместо этого использовать TimeUnit.SECONDS.sleep(2); На самом деле, причина очень проста, у первого нет четкого описания модуля, а у второго очень ясно выражает второе На самом деле, внутренняя реализация последнего по-прежнему вызывает Thread.sleep(2000);, но для более ясного написания семантики кода рекомендуется использовать TimeUnit.SECONDS.sleep(2); обратите внимание, что TimeUnit является типом перечисления. В дополнение к сценарию блокирующего прерывания мы также можем столкнуться с потоками, которые находятся в работающем и неблокирующем состоянии. В этом случае прямой вызов Thread.interrupt() для прерывания потока не получит никакого ответа. Следующий код не будет иметь возможность прерывать поток в неблокирующем состоянии:

public class InterruputThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(){
            @Override
            public void run(){
                while(true){
                    System.out.println("未被中断");
                }
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();

        /**
         * 输出结果(无限执行):
             未被中断
             未被中断
             未被中断
             ......
         */
    }
}

Хотя мы вызвали метод прерывания, поток t1 не был прерван, потому что поток в неблокирующем состоянии требует от нас ручного обнаружения прерывания и завершения программы.Улучшенный код выглядит следующим образом:

public class InterruputThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(){
            @Override
            public void run(){
                while(true){
                    //判断当前线程是否被中断
                    if (this.isInterrupted()){
                        System.out.println("线程中断");
                        break;
                    }
                }

                System.out.println("已跳出循环,线程中断!");
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();

        /**
         * 输出结果:
            线程中断
            已跳出循环,线程中断!
         */
    }
}

Мы используем метод экземпляра isInterrupted в коде, чтобы определить, был ли поток прерван. Если он прерван, он выйдет из цикла, чтобы завершить поток. Обратите внимание, что вызов interrupt() в неблокирующем состоянии не вызовет состояние прерывания для сброса. Таким образом, можно кратко суммировать две ситуации прерывания: во-первых, когда поток находится в состоянии блокировки или пытается выполнить блокирующую операцию, мы можем использовать метод экземпляра interrupt() для прерывания потока, и будет создано прерываниеException. выбрасывается после выполнения операции прерывания.Исключение (исключение должно быть перехвачено и не может быть выброшено) и сброс состояния прерывания.Другое заключается в том, что во время выполнения потока мы также можем вызвать метод экземпляра interrupt() для прерывания thread, но в то же время мы должны вручную оценивать состояние прерывания и писать код, который прерывает поток (фактически, код, который завершает тело метода run). Иногда нам может понадобиться учитывать две вышеупомянутые ситуации при кодировании, поэтому мы можем написать следующее:

public void run(){
    try {
    //判断当前线程是否已中断,注意interrupted方法是静态的,执行后会对中断状态进行复位
    while (!Thread.interrupted()) {
        TimeUnit.SECONDS.sleep(2);
    }
    } catch (InterruptedException e) {

    }
}

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

public class SynchronizedBlocked implements Runnable{

    public synchronized void f() {
        System.out.println("Trying to call f()");
        while(true) // Never releases lock
            Thread.yield();
    }

    /**
     * 在构造器中创建新线程并启动获取对象锁
     */
    public SynchronizedBlocked() {
        //该线程已持有当前实例锁
        new Thread() {
            public void run() {
                f(); // Lock acquired by this thread
            }
        }.start();
    }
    public void run() {
        //中断判断
        while (true) {
            if (Thread.interrupted()) {
                System.out.println("中断线程!!");
                break;
            } else {
                f();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        SynchronizedBlocked sync = new SynchronizedBlocked();
        Thread t = new Thread(sync);
        //启动后调用f()方法,无法获取当前实例锁处于等待状态
        t.start();
        TimeUnit.SECONDS.sleep(1);
        //中断线程,无法生效
        t.interrupt();
    }
}

Мы создаем новый поток в конструкторе SynchronizedBlocked и запускаем вызов f() для получения текущей блокировки экземпляра.Поскольку SynchronizedBlocked сам по себе является потоком, f() также вызывается в своем методе запуска после запуска, но поскольку блокировка объекта заблокирован другими потоками Занято, поток t может только дождаться блокировки, в это время мы вызываем t.interrupt(), но поток не может быть прерван.

5.3 Ожидание механизма пробуждения и синхронизация

Так называемый механизм ожидающего пробуждения в основном относится к методам notify/notifyAll и wait.При использовании этих трех методов вы должны находиться в синхронизированном блоке кода или синхронизированном методе, иначе будет сгенерировано исключение IllegalMonitorStateException, потому что эти три метода объект монитора текущего объекта должен быть получен до метода, то есть методы notify/notifyAll и wait зависят от объекта монитора.В предыдущем анализе мы знали, что монитор существует в слове Mark заголовка объекта (хранит указатель ссылки на монитор), а ключевое слово synchronized может получить монитор, поэтому методы notify/notifyAll и wait должны вызываться в блоке кода synchronized или в методе synchronized.

Важно понимать, что, в отличие от метода сна, поток будет приостановлен после завершения вызова метода ожидания.Однако метод ожидания освобождает текущую блокировку монитора (монитор) до тех пор, пока поток не вызовет метод notify/notifyAll, прежде чем продолжить выполнение, в то время как метод сна только переводит поток в спящий режим и не освобождает блокировку. При этом после вызова метода notify/notifyAll блокировка монитора снимается не сразу, а снимается автоматически после выполнения соответствующего метода synchronized(){}/synchronized.