Введение в различные блокировки в JAVA

Java

Содержание этого раздела:

введение в блокировку java

Замок смещения, легкий замок, тяжелый замок

Эти три блокировки относятся конкретно к статусу синхронизированных блокировок, а статус блокировки представлен полем отметки работы в заголовке объекта.

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

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

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

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

Тяжелый замок:

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

Реентерабельные блокировки, нереентерабельные блокировки

Повторно входящие блокировки:

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

Блокировки без повторного входа:

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

общая блокировка, эксклюзивная блокировка

Общий замок:

Общая блокировка означает, что мы можем получить одну и ту же блокировку несколькими потоками одновременно.Наиболее типичным примером является блокировка чтения в блокировке чтения-записи.

Эксклюзивный замок:

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

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

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

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

Несправедливая блокировка:

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

Пессимистический замок, оптимистичный замок

Пессимистический замок:

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

Оптимистичная блокировка:

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

блокировка вращения, блокировка без вращения

Спинлок:

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

Без спин-блокировки:

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

Прерываемый замок, непрерываемый замок

Прерываемый замок:

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

Бесперебойные замки:

Synchronized — это непрерываемая блокировка, что означает, что после применения блокировки другие логические операции могут выполняться только после получения блокировки.

Введение синхронизированной блокировки

Что такое синхронизированный замок

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

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

public class TestSync {
    public void sync1(){
        synchronized (this){
            int ss = 10;
            System.out.println(ss);
        }
    }
}

Как показано в приведенном выше коде, метод sync1() в определенном нами классе TestSync содержит блок кода синхронизации, и мы передаем инструкцию:javap -verbose TestSync.classСодержание разборки, соответствующее способу просмотра, следующее:

public void sync1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter    //加锁
         4: bipush        10
         6: istore_2
         7: getstatic     #2  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: iload_2
        11: invokevirtual #3   // Method java/io/PrintStream.println:(I)V
        14: aload_1
        15: monitorexit    //解锁
        16: goto          24
        19: astore_3
        20: aload_1
        21: monitorexit    解锁
        22: aload_3
        23: athrow
        24: return

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

монитор введите значение:
Каждый объект поддерживает счетчик.Счетчик незаблокированных объектов равен 0. Если поток, выполняющий команду monitorenter, попытается завладеть монитором, возможны следующие три ситуации:

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

значение выхода монитора:
Функция monitorexit заключается в уменьшении счетчика монитора на 1 до тех пор, пока он не уменьшится до 0, что означает освобождение монитора. Тогда другие потоки могут попытаться завладеть монитором в это время.

Применение синхронизированного ключевого слова в синхронизированном методе:

Давайте посмотрим, как выглядит дизассемблированное содержимое метода синхронизации.

public class TestSync {
    public synchronized void sync2(){
        int aa = 10;
        System.out.println(aa);
    }
}

Код дизассемблирования выглядит следующим образом:

public synchronized void sync2();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=2, args_size=1
         0: bipush        10
         2: istore_1
         3: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         6: iload_1
         7: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        10: return

Из приведенного выше мы видим, что метод синхронизации кода отличается от блока синхронизации, а блок синхронизации Зависимый monitorenter и monitorexit для блокировки и разблокировки, метод синхронизации будет более ACC_SYNCHRONIZED модификаторов флагов, чтобы указать, что он является синхронным методом. Таким образом, будет синхронизирован модифицированный метод Флаг ACC_SYNCHRONIZED, когда потоки обращаются к методу проверки того, будет ли метод нести флаг ACC_SYNCHRONIZED, Затем вы переходите к блокировке монитора, а затем возвращаетесь к блокировке реализации метода, метода выполнения после блокировки монитора освобождения.

Синхронизированное ключевое слово и сравнение интерфейса блокировки

Тот же пункт:

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

разница:

  • Разница в использовании: ключевое слово synchronized можно добавить в метод без указания объекта блокировки, или вы можете создать новый блок кода синхронизации и настроить объект блокировки монитора, в то время как интерфейс блокировки должен отображать объект блокировки блокировки, чтобы начать блокировку lock() и разблокировка unLock(), и, как правило, обязательно разблокируйте с помощью unLock() в блоке finally, чтобы предотвратить взаимоблокировки.
  • Порядок добавления и разблокировки различен.Замок можно разблокировать в разном порядке.Например, если мы сначала получаем замок A, а затем получаем замок B, то при разблокировке мы можем сначала разблокировать замок A, а затем разблокировать замок B, но синхронизация должна быть разблокирована в следующем порядке, например, после получения блокировки A, а затем получения блокировки B, затем разблокировка заключается в разблокировке сначала блокировки B, а затем разблокировки блокировки A.
  • Synchronized менее гибок, чем Lock. После того, как синхронизированная блокировка получена потоком, другие потоки могут только заблокироваться и ждать освобождения блокировки. Если поток, удерживающий блокировку, выполняется в течение длительного времени, эффективность работы всей программы будет очень низким, и если блокировка не будет, другие потоки будут продолжать ждать, чтобы снять блокировку.По сравнению с методом lockInterruptently из Lock, если вы чувствуете, что поток, удерживающий блокировку, выполняется слишком долго, его можно прервать и Вы также можете использовать tryLock(), чтобы попытаться получить блокировку и выполнить другую логику, если она не может быть получена.
  • Некоторые классы реализации интерфейса Lock, такие как блокировка чтения в блокировке чтения-записи, могут удерживаться несколькими потоками, а синхронизированные могут удерживаться только одним потоком.
  • Синхронизированная — это встроенная блокировка (блокировка монитора), реализованная JVM и разблокированная. Она также делится на предвзятые блокировки, облегченные блокировки и тяжелые блокировки. Интерфейс блокировки имеет разные базовые принципы в зависимости от разных реализаций.
  • Блокировка может быть установлена ​​независимо от того, справедлива блокировка или нет, синхронизация не может быть установлена
  • После java6 jdk много оптимизировал synchronized, поэтому производительность synchronized не хуже, чем у Lock.

Честные и нечестные замки

Честные и нечестные замки

Справедливые блокировки относятся к потокам, которые выделяют блокировки в порядке запросов, а недобросовестные блокировки относятся к тому, что блокировки не выделяются в порядке, запрошенном потоками, но несправедливые блокировки не являются полностью случайными выделениями, а выполняются «в нужное время».

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

Отображение эффекта справедливой блокировки и несправедливой блокировки

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

Lock lock=new ReentrantLock(false);

Отображение кода честной блокировки:

/**
 * 描述:演示公平锁,分别展示公平和不公平的情况,
 * 非公平锁会让现在持有锁的线程优先再次获取到锁。
 * 代码借鉴自Java并发编程实战手册2.7
 */
​
public class FairAndNoFair {
    public static void main(String[] args) {
        PrintQueue printQueue = new PrintQueue();
​
        Thread[] threads= new Thread[10];
        for(int i=0;i<10;i++){
            threads[i] = new Thread(new Job(printQueue),"Thread "+ i);
        }
​
        for (int i = 0; i < 10; i++) {
            threads[i].start();
            try {
                Thread.sleep(100);//为了保证执行顺序
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Job implements Runnable{
    private PrintQueue printQueue;
    public Job(PrintQueue printQueue){
        this.printQueue=printQueue;
    }
    @Override
    public void run() {
        System.out.printf("%s: Going to print a job\n", Thread.currentThread().getName());
        printQueue.printJob();
        System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
    }
}
public class PrintQueue {
    private final Lock lock=new ReentrantLock(false);
​
    public void printJob(){
        lock.lock();
​
        try{
            Long duration = (long) (Math。random()*10000);
            System.out.printf("%s:First PrintQueue: Printing a Job during %d seconds\n",
                    Thread.currentThread().getName(), (duration / 1000));
            Thread.sleep(duration);
        } catch (InterruptedException e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
​
​
        lock.lock();
        try{
            Long duration = (long) (Math.random()*10000);
            System.out.printf("%s:Second PrintQueue: Printing a Job during %d seconds\n",
                    Thread.currentThread().getName(), (duration / 1000));
            Thread.sleep(duration);
        } catch (InterruptedException e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

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

Thread 0: Going to print a job
Thread 0:First PrintQueue: Printing a Job during 9 seconds
Thread 1: Going to print a job  //线程1-9进入等待队列排队
Thread 2: Going to print a job
Thread 3: Going to print a job
Thread 4: Going to print a job
Thread 5: Going to print a job
Thread 6: Going to print a job
Thread 7: Going to print a job
Thread 8: Going to print a job
Thread 9: Going to print a job
Thread 1:First PrintQueue: Printing a Job during 5 seconds//线程0执行完释放了锁,线程1开始执行
Thread 2:First PrintQueue: Printing a Job during 1 seconds
Thread 3:First PrintQueue: Printing a Job during 9 seconds
Thread 4:First PrintQueue: Printing a Job during 7 seconds
Thread 5:First PrintQueue: Printing a Job during 8 seconds
Thread 6:First PrintQueue: Printing a Job during 5 seconds
Thread 7:First PrintQueue: Printing a Job during 2 seconds
Thread 8:First PrintQueue: Printing a Job during 9 seconds
Thread 9:First PrintQueue: Printing a Job during 7 seconds
Thread 0:Second PrintQueue: Printing a Job during 0 seconds
Thread 1:Second PrintQueue: Printing a Job during 6 seconds
Thread 0: The document has been printed
Thread 1: The document has been printed
Thread 2:Second PrintQueue: Printing a Job during 4 seconds
Thread 2: The document has been printed
Thread 3:Second PrintQueue: Printing a Job during 4 seconds
Thread 3: The document has been printed
Thread 4:Second PrintQueue: Printing a Job during 1 seconds
Thread 4: The document has been printed
Thread 5:Second PrintQueue: Printing a Job during 3 seconds
Thread 5: The document has been printed
Thread 6:Second PrintQueue: Printing a Job during 0 seconds
Thread 6: The document has been printed
Thread 7:Second PrintQueue: Printing a Job during 1 seconds
Thread 7: The document has been printed
Thread 8:Second PrintQueue: Printing a Job during 5 seconds
Thread 8: The document has been printed
Thread 9:Second PrintQueue: Printing a Job during 5 seconds
Thread 9: The document has been printed
Process finished with exit code 0

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

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

Thread 0: Going to print a job
Thread 0:First PrintQueue: Printing a Job during 5 seconds
Thread 1: Going to print a job
Thread 2: Going to print a job
Thread 3: Going to print a job
Thread 4: Going to print a job
Thread 5: Going to print a job
Thread 6: Going to print a job
Thread 7: Going to print a job
Thread 8: Going to print a job
Thread 9: Going to print a job
Thread 0:Second PrintQueue: Printing a Job during 2 seconds //线程0直接释放锁又获取了锁,体现了非公平锁
Thread 0: The document has been printed
Thread 1:First PrintQueue: Printing a Job during 9 seconds
Thread 1:Second PrintQueue: Printing a Job during 3 seconds
Thread 1: The document has been printed
Thread 2:First PrintQueue: Printing a Job during 0 seconds
Thread 3:First PrintQueue: Printing a Job during 0 seconds
Thread 3:Second PrintQueue: Printing a Job during 7 seconds
Thread 3: The document has been printed
Thread 4:First PrintQueue: Printing a Job during 3 seconds
Thread 4:Second PrintQueue: Printing a Job during 8 seconds
Thread 4: The document has been printed
Thread 5:First PrintQueue: Printing a Job during 6 seconds
Thread 5:Second PrintQueue: Printing a Job during 1 seconds
Thread 5: The document has been printed
Thread 6:First PrintQueue: Printing a Job during 0 seconds
Thread 6:Second PrintQueue: Printing a Job during 7 seconds
Thread 6: The document has been printed
Thread 7:First PrintQueue: Printing a Job during 8 seconds
Thread 7:Second PrintQueue: Printing a Job during 1 seconds
Thread 7: The document has been printed
Thread 8:First PrintQueue: Printing a Job during 9 seconds
Thread 8:Second PrintQueue: Printing a Job during 8 seconds
Thread 8: The document has been printed
Thread 9:First PrintQueue: Printing a Job during 5 seconds
Thread 9:Second PrintQueue: Printing a Job during 5 seconds
Thread 9: The document has been printed
Thread 2:Second PrintQueue: Printing a Job during 3 seconds
Thread 2: The document has been printed
​
Process finished with exit code 0

Как видно на рисунке выше, после того, как поток 0 снимает блокировку, он сразу же получает блокировку и продолжает выполнение, при этом происходит явление захвата блокировки и вставки в очередь (в это время в очереди ожидания уже есть потоки 1-9). а потом ждет).

Справедливые и несправедливые блокировки имеют недостатки

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

Анализ исходного кода справедливой и недобросовестной блокировки
Во-первых, как честные, так и нечестные блокировки наследуют внутренний класс Sync в классе ReentrantLock.Этот класс Sync наследует AQS (AbstractQueuedSynchronizer).Код класса Sync выглядит следующим образом:

//源码中可以看出Sync继承了AbstractQueuedSynchronizer类
abstract static class Sync extends AbstractQueuedSynchronizer {...}
//Sync有公平锁FairSync和非公平锁NonFairSync两个子类:
static final class NonfairSync extends Sync {。。。}
static final class FairSync extends Sync {。。}

Исходный код блокировки получения справедливой блокировки:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    //这里和非公平锁对比多了个!hasQueuedPredecessors()判断
        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;
}

Исходный код блокировки получения недобросовестной блокировки:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

Для сравнения можно обнаружить, что разница между честными блокировками и нечестными блокировками заключается в основном в получении блокировок. Справедливая блокировка имеет дополнительное суждение о том, что hasQueuedPredecessors() является ложным, метод hasQueuedPredecessors() Это нужно для того, чтобы определить, есть ли потоки, ожидающие в очереди ожидания. Если да, то текущий поток не может пытаться получить блокировку. Независимо от того, есть ли ожидающий поток, сначала попытайтесь получить блокировку, а затем встаньте в очередь, если она не может быть получена Метод tryLock() внутренне вызывает sync.nonfairTryAcquire(1), который не Честная блокировка, поэтому, даже если установлен честный режим, используйте tryLock() для перехода из очереди.

Блокировка чтения-записи

Зачем устанавливать блокировку чтения-записи

Прежде всего, блокировка чтения-записи предназначена для повышения эффективности системы. В то время как обычный ReentrantLock может гарантировать потокобезопасность, Однако при наличии нескольких операций чтения прямое использование ReentrantLock приведет к значительной трате системных ресурсов. Кроме того, операция записи небезопасна: при одновременной записи или одновременном чтении с операцией записи могут возникнуть проблемы с безопасностью потоков. Затем играет роль установленная блокировка чтения-записи.Блокировка чтения-записи поддерживает параллельное чтение, чтобы повысить эффективность чтения и обеспечить безопасность операций записи.

Правила блокировки чтения-записи

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

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

ReentrantReadWriteLock используется здесь для демонстрации ReentrantReadWriteLock — это класс реализации ReadWriteLock Два основных метода: readLock() для получения блокировки чтения и writeLock() для получения блокировки записи. Здесь блокировка чтения-записи в ReadWriteLock используется для одновременного чтения и записи.Код показан следующим образом:

public class ReadWriteLock {
    //定义读写锁
    private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
    //获取读锁
    private static final ReentrantReadWriteLock.ReadLock readLock= reentrantReadWriteLock.readLock();
    //获取写锁
    private static final ReentrantReadWriteLock.WriteLock writeLock= reentrantReadWriteLock.writeLock();
​
    public static void read(){
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() +"得到读锁,正在读取");
            Thread.sleep(500);
        }catch (InterruptedException e){
            e.printStackTrace();
        } finally {
            System.out.println("释放读锁");
            readLock.unlock();
        }
    }
​
    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }
​
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> read()).start();
        new Thread(() -> read()).start();
        new Thread(() -> write()).start();
        new Thread(() -> write()).start();
    }
}

Результаты приведены ниже:

Thread-0得到读锁,正在读取
Thread-1得到读锁,正在读取
释放读锁
释放读锁
Thread-2得到写锁,正在写入
Thread-2释放写锁
Thread-3得到写锁,正在写入
Thread-3释放写锁

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

Чтение стратегии сокращения очереди блокировок

Во-первых, блокировка чтения-записи ReentrantReadWriteLock поддерживает честные и нечестные блокировки, которые можно установить следующим образом:

//后面的boolean值用来设置公平锁、非公平锁,其中的false设置为非公平锁,设置为true就是公平锁,
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);

Если установлено значение Fair Lock, соответствующая блокировка чтения-записи реализуется следующим образом:

static final class FairSync extends Sync {
    private static final long serialVersionUID = -2274990926593161451L;
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

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

Если установлено значение false для несправедливой блокировки, соответствующая реализация будет следующей:

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        final boolean readerShouldBlock() {
            /* As a heuristic to avoid indefinite writer starvation,
             * block if the thread that momentarily appears to be head
             * of queue, if one exists, is a waiting writer。  This is
             * only a probabilistic effect since a new reader will not
             * block if there is a waiting writer behind other enabled
             * readers that have not yet drained from the queue。
             */
            return apparentlyFirstQueuedIsExclusive();
        }
}

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

Сценарий: если есть потоки 1 и 2, читающие одновременно, и 1 и 2 уже удерживают блокировку чтения, то поток 3 хочет записать, поэтому поток 3 входит в очередь ожидания, а поток 4 внезапно переходит в очередь, чтобы получить блокировка чтения. На данный момент есть две стратегии:

  • Разрешение обрезки очереди, позволяющее потоку 4 получить блокировку чтения, а потоку 1 и потоку 2 читать вместе, по-видимому, повышает эффективность чтения, но есть серьезная проблема, то есть если следующие потоки всегда хотят получить блокировку чтения. поток, то поток 3 никогда не получит возможности выполниться, тогда он попадет в состояние "голодания" и не будет выполняться в течение длительного времени.
  • Сокращение очереди не разрешено. В это время, если новый поток 4 хочет получить блокировку чтения, он должен ждать в очереди. В соответствии с этой стратегией приоритет отдается потоку 3 или потоку 4, и указанное выше состояние «голодания» может быть избегать до потока 3. В конце выполнения поток 4 имеет возможность выполниться.

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

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

public class ReadWriteLock {
    //定义读写锁
    private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
    //获取读锁
    private static final ReentrantReadWriteLock.ReadLock readLock= reentrantReadWriteLock.readLock();
    //获取写锁
    private static final ReentrantReadWriteLock.WriteLock writeLock= reentrantReadWriteLock.writeLock();
​
    public static void read(){
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() +"得到读锁,正在读取");
            Thread.sleep(500);
        }catch (InterruptedException e){
            e.printStackTrace();
        } finally {
            System.out.println("释放读锁");
            readLock.unlock();
        }
    }
​
    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }
​
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> read()).start();
        new Thread(() -> read()).start();
        new Thread(() -> write()).start();
        //以上代码没有改变,这里换成了读锁
        new Thread(() -> read()).start();
    }
}

Результаты приведены ниже:

Thread-0得到读锁,正在读取
Thread-1得到读锁,正在读取
释放读锁
释放读锁
Thread-2得到写锁,正在写入
Thread-2释放写锁
Thread-3得到读锁,正在读取
释放读锁

Из текущих результатов видно, что ReentrantReadWriteLock выбрал стратегию, которая не позволяет сократить очередь.

Модернизация и понижение блокировки чтения-записи

записать понижение блокировкиПонижение блокировки записи, код показывает:

//定义读写锁
private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//获取读锁
private static final ReentrantReadWriteLock.ReadLock readLock= reentrantReadWriteLock.readLock();
//获取写锁
private static final ReentrantReadWriteLock.WriteLock writeLock= reentrantReadWriteLock.writeLock();
​
//锁的降级
public static void downgrade(){
    System.out.println(Thread.currentThread().getName()+"尝试获取写锁");
    writeLock.lock();//获取写锁
    try {
        System.out.println(Thread.currentThread().getName()+"获取了写锁");
        //在不释放写锁的情况下直接获取读锁,这就是读写锁的降级
        readLock.lock();
        System.out.println(Thread.currentThread().getName()+"获取了读锁");
    }finally {
        System.out.println(Thread.currentThread().getName()+"释放了写锁");
        //释放了写锁,但是依然持有读锁,这里不释放读锁,导致后面的线程无法获取写锁
        writeLock.unlock();
    }
}

public static void main(String[] args) {
    new Thread(() -> downgrade()).start();
    new Thread(() -> downgrade()).start();
}

Результат запуска приведенного выше рисунка выглядит следующим образом:

image.pngНа рисунке мы видим, что поток 0 может получить блокировку чтения, удерживая блокировку записи, что является понижением блокировки записи, потому что за потоком 0 размещается только блокировка записи, Блокировка чтения не снимается, поэтому следующий поток 1 не может получить блокировку записи, поэтому программа продолжает блокироваться.

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

private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
//获取读锁
private static final ReentrantReadWriteLock.ReadLock readLock= reentrantReadWriteLock.readLock();
//获取写锁
private static final ReentrantReadWriteLock.WriteLock writeLock= reentrantReadWriteLock.writeLock();

//读锁升级
public static void upgarde(){
    System.out.println(Thread.currentThread().getName()+"尝试获取读锁");
    readLock.lock();
    try{
        System.out.println(Thread.currentThread().getName()+"获取到了读锁");
        System.out.println(Thread.currentThread().getName()+"阻塞获取写锁");
        //在持有读锁的情况下获取写锁,此处会阻塞,表示不支持读锁升级到写锁
        writeLock.lock();//此处会阻塞
        System.out.println(Thread.currentThread().getName()+"获取到了写锁");
​
    }finally {
        readLock.unlock();
    }
}
​
public static void main(String[] args) {
    new Thread(() -> upgarde()).start();
    new Thread(() -> upgarde()).start();
}

Результаты приведены ниже:

image.png

На приведенном выше рисунке видно, что и поток 0, и поток 1 могут успешно получить блокировку чтения, но они блокируются при обновлении блокировки для получения блокировки записи, поскольку ReentrantReadWriteLock не поддерживает обновление блокировки чтения. к блокировке записи. Поскольку блокировки чтения могут удерживаться несколькими потоками, а блокировки записи — только одним потоком, и невозможно одновременно удерживать и блокировки чтения, и блокировки записи, именно по этой причине в процессе обновления запись locks , вам нужно дождаться снятия всех блокировок чтения, прежде чем можно будет выполнить эскалацию блокировок.

Например, например, все три потока ABC удерживают блокировки чтения. Если поток A хочет усилить блокировку, он должен дождаться, пока B и C снимут блокировку чтения. В это время поток A может успешно обновиться и получить доступ к записи. замок.

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

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

Введение в спин-блокировки

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

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

image.png

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

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

Недостатки спин-блокировок
Хотя спин-блокировка позволяет избежать накладных расходов на переключение потоков, она также приводит к новым накладным расходам, поскольку ему нужно продолжать цикл, чтобы попытаться получить блокировку. пустая трата ресурса.

Поэтому спин-блокировки подходят для сценариев, где степень параллелизма не особенно высока, а поток удерживает блокировку на короткое время. Например, большинство атомарных классов в пакете java.util.concurrent основаны на реализации спин-блокировки, такой как AtomicInteger, мы рассмотрим его метод getAndIncrement() следующим образом:

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
​
        return var5;
}

Очевидно, что цикл do...while() является циклической операцией.Если в процессе модификации встречаются другие потоки и модификация не успешна, повторение цикла будет выполняться до тех пор, пока модификация не будет успешной.

Как настроить реентерабельную спин-блокировку

Код реализации выглядит следующим образом:

//自定义实现可重入的自旋锁
public class CustomReentrantSpinLock {
    private AtomicReference<Thread> owner=new AtomicReference<>();
    private int count = 0;//重入次数
​
    public void lock() {
        Thread t = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"lock了");
        if (t == owner.get()) {
            ++count;
            return;
        }
​
        //自旋获取锁
        while (!owner.compareAndSet(null, t)) {
            System.out.println(Thread.currentThread().getName()+"自旋了");
        }
    }
​
    public void unLock(){
        Thread t=Thread.currentThread();
        //只有当前线程才能解锁
        if(t == owner.get()){
            if(count >0){
                --count;
            } else {
                owner.set(null);
            }
        }
    }
​
    public static void main(String[] args) {
        CustomReentrantSpinLock spinLock=new CustomReentrantSpinLock();
        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"开始尝试获取自旋锁");
                spinLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName()+"获取到了自旋锁");
                    Thread.sleep(1);
                }catch (InterruptedException e){
                    e.printStackTrace();
                } finally {
                    spinLock.unLock();
                    System.out.println(Thread.currentThread().getName()+"释放了自旋锁");
                }
            }
        };
        Thread thread1=new Thread(runnable);
        Thread thread2=new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

Результаты приведены ниже:

Thread-0开始尝试获取自旋锁
Thread-1开始尝试获取自旋锁
Thread-0获取到了自旋锁
Thread-1自旋了
.
.
.
Thread-1自旋了
Thread-0释放了了自旋锁
Thread-1获取到了自旋锁
Thread-1释放了了自旋锁

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

JVM-оптимизация блокировок

Начиная с jdk1.6, виртуальная машина HotSpot сделала много оптимизаций для синхронизации, включая адаптивный самовыбор, устранение блокировок, огрубление блокировки, предвзятую блокировку, упрощенную блокировку и т. д., что значительно повышает производительность синхронизированной блокировки.

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

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

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

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

@Override
public synchronized StringBuffer append(Object obj) {
    toStringCache = null;
    super.append(String。valueOf(obj));
    return this;
}

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

замок огрубление

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

public void lockCoarsening() {
    synchronized (this) {
        //do something
    }
    synchronized (this) {
        //do something
    }
    synchronized (this) {
        //do something
    }
}

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

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

for (int i = 0; i < 1000; i++) {
    synchronized (this) {
        //do something
    }
}

Огрубление блокировки включено по умолчанию, и эта функция отключается с помощью -XX:-EliminateLocks.

Замок смещения, легкий замок, тяжелый замок

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

Путь к эскалации блокировки

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

image.png

-END

Если вам это нравится, отсканируйте QR-код ниже или найдите «Внутренние навыки программиста» и подпишитесь на меня в WeChat.

image.png