Не знаете, что такое замок? Посмотри на это и ты поймешь

Java

Классификация блокировки Java

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

  • Нужно ли потоку блокировать ресурс, можно разделить на悲观锁и乐观锁
  • Из ресурса был заблокирован, может ли поток заблокирован, можно разделить на自旋锁
  • Доступ к ресурсам одновременно из нескольких потоков, то есть Synchronized можно разделить на无锁,偏向锁,轻量级锁и重量级锁
  • По справедливости замок можно разделить на公平锁и非公平锁
  • В зависимости от того, была ли блокировка получена повторно, ее можно разделить на可重入锁и不可重入锁
  • От того, могут ли несколько потоков получить одну и ту же блокировку, разделяется на共享锁и排他锁

Далее мы подробно опишем классификацию каждого замка.

Нужно ли потоку блокировать ресурс

Java делится на две категории в зависимости от того, заблокирован ли ресурс乐观锁и悲观锁, оптимистическая блокировка и пессимистическая блокировка - это не настоящая блокировка, а дизайнерская идея. Оптимистическая блокировка и пессимистическая блокировка очень важны для понимания многопоточности и базы данных Java. Давайте обсудим эти два метода реализации. Различия, преимущества и недостатки

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

悲观锁Это своего рода пессимистическое мышление. Оно всегда думает, что может произойти наихудшая ситуация. Оно думает, что данные, вероятно, будут изменены другими, поэтому пессимистическая блокировка всегда будет блокировать данные, когда она удерживает данные.资源или数据Чтобы заблокировать, этот другой поток будет заблокирован, когда вы запросите этот ресурс, пока вы не дождетесь, пока пессимистическая блокировка не поместит освобождение ресурса. В традиционной реляционной базе данных много таких механизмов блокировки, **, как блокировка строки, блокировка таблицы и т. д., блокировка чтения, блокировка записи и т. д., все блокировки перед выполнением операций. ** Пессимистичные блокировки часто полагаются на функцию блокировки самой базы данных.

на ЯвеSynchronizedиReentrantLockОжидание эксклюзивной блокировки (exclusive lock) — это тоже реализация идеи пессимистической блокировки, потому что Synchronzied и ReetrantLock будут пытаться заблокировать независимо от того, держат они ресурсы или нет, опасаясь, что их любимое детище отнимут другие.

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

Идея оптимистической блокировки противоречит идее пессимистической блокировки, она всегда считает, что ресурсы и данные не будут изменены другими, поэтому чтение не будет заблокировано, но оптимистическая блокировка будет определять, были ли текущие данные модифицируется при написании операций.(конкретно как судить об этом мы поговорим позже). Обычно существует две реализации оптимистической блокировки:版本号机制иCAS实现. Оптимистическая блокировка в основном подходит для типов приложений с несколькими уровнями, которые могут повысить пропускную способность.

на Явеjava.util.concurrent.atomicКласс атомарной переменной под пакетом является реализацией реализации оптимистической блокировки.

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

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

select * from student where name="cxuan" for update

Этот sql-оператор выбирает запись с name="cxuan" из таблицы Student и блокирует ее, тогда другие операции записи не будут работать с этими данными, пока транзакция не будет зафиксирована, что играет исключительную и исключительную роль.

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

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

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

Реализация оптимистической блокировки

Обычно существует два способа реализации оптимистической блокировки:版本号机制иCAS(Compare-and-Swap,即比较并替换)Реализация алгоритма.

Механизм номера версии

Механизм номера версии заключается в добавленииversionЭто реализуется полем, указывающим, сколько раз данные были изменены.Когда операция записи выполнена и запись прошла успешно, версия = версия + 1. Когда поток А захочет обновить данные, он также прочитает значение версии при чтении данных.При отправке обновления, если только что прочитанное значение версии равно значению версии в текущей базе данных, оно будет обновлено; в противном случае операция обновления будет повторяться до тех пор, пока обновление не будет успешным.

Давайте возьмем приведенную выше финансовую систему в качестве примера, чтобы кратко описать этот процесс.

  • В системе затрат есть таблица данных, и в таблице есть два поля:金额иversion, атрибут суммы может быть изменен в режиме реального времени, а версия представляет собой версию суммы, которая меняется каждый раз.Общая стратегия заключается в том, что при изменении суммы версия принимает инкрементную стратегию, основанную на предыдущей номер версии каждый раз + 1.
  • После понимания базовой ситуации и основной информации давайте посмотрим на процесс: после того, как компания получила деньги обратно, она должна поставить деньги в хранилище, если в хранилище есть 100 юаней.
    • Затем начните транзакцию 1: когда кассир-мужчина выполняет операцию записи, он сначала проверит (прочитает), сколько денег осталось в хранилище.В это время, прочитав, что в хранилище есть 100 юаней, он может выполнить операцию записи и сохранить деньги в базе данных.Деньги обновляются до 120 юаней, транзакция отправляется, деньги в хранилище 100 -> 120, а номер версии версии 0 -> 1.
    • Открытая транзакция 2: После того, как женщина-кассир получит запрос на выплату зарплаты сотруднику, ей нужно сначала выполнить запрос на чтение, проверить, сколько денег осталось в хранилище, и какой номер версии на данный момент, а затем взять Вывести зарплату сотрудника из хранилища на оплату.Зафиксировать транзакцию, после успешной версии +1, в это время версия 1 -> 2.

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

  • Как только транзакция открыта, кассир-мужчина сначала выполняет операцию чтения, вынимает сумму и номер версии и выполняет операцию записи.

    begin
    update 表 set 金额 = 120,version = version + 1 where 金额 = 100 and version = 0
    

    В этот момент количество изменено на 120, номер версии равен 1, и транзакция не была зафиксирована.

    Когда транзакция 2 открыта, женщина-кассир впервые выполняет операцию чтения, вынимает количество номеров и версий и выполняет операцию записи

    begin
    update 表 set 金额 = 50,version = version + 1 where 金额 = 100 and version = 0
    

    В этот момент количество изменяется на 50, номер версии изменяется на 1, и транзакция не фиксируется.

    Теперь зафиксируйте транзакцию номер один, количество изменится на 120, версия изменится на 1, и транзакция будет зафиксирована. В идеале должно стать количество = 50, номер версии = 2, но на самом деле обновление второй транзакции основано на сумме 100 и номере версии 0, поэтому транзакция два не будет успешно зафиксирована и должна быть повторно прочитана Сумма и номер версии, напишите еще раз.

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

CAS-алгоритм

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

CAS естьcompare and swap(比较与交换), является хорошо известным алгоритмом без блокировки. То есть достичь переменной синхронизации между несколькими потоками без использования блокировок, то есть добиться переменной синхронизации без блокировки потоков, поэтому это также называется неблокирующей синхронизацией (Non-blocking Synchronization).

Java поддерживается начиная с JDK1.5,java.util.concurrentПакет предоставляет множество классов для параллельного программирования, а также обеспечивает поддержку алгоритмов CAS.AtomicНекоторые атомарные классы для начала используют CAS в качестве своей реализации. Использование этих классов повысит производительность на машинах с многоядерными процессорами.

Чтобы доказать их атомарность, их необходимо заблокировать, используяSynchronziedилиReentrantLockМы представляем их к реализации пессимистичных замков, теперь мы обсудим блокировку оптимизма, затем, какой способ обеспечить их атомность? Пожалуйста, смотрите следующую часть

В CAS задействованы три элемента:

  • Значение памяти V, которое необходимо прочитать и записать
  • значение для сравнения A
  • новое значение B для записи

Измените значение памяти V на B тогда и только тогда, когда ожидаемое значение A и значение памяти V совпадают, в противном случае ничего не делайте.

Начнем с java.util.concurrentAtomicIntegerВ качестве примера посмотрим, как гарантируется потокобезопасность без блокировок.

public class AtomicCounter {

    private AtomicInteger integer = new AtomicInteger();

    public AtomicInteger getInteger() {
        return integer;
    }

    public void setInteger(AtomicInteger integer) {
        this.integer = integer;
    }

    public void increment(){
        integer.incrementAndGet();
    }

    public void decrement(){
        integer.decrementAndGet();
    }

}

public class AtomicProducer extends Thread{

    private AtomicCounter atomicCounter;

    public AtomicProducer(AtomicCounter atomicCounter){
        this.atomicCounter = atomicCounter;
    }

    @Override
    public void run() {
        for(int j = 0; j < AtomicTest.LOOP; j++) {
            System.out.println("producer : " + atomicCounter.getInteger());
            atomicCounter.increment();
        }
    }
}

public class AtomicConsumer extends Thread{

    private AtomicCounter atomicCounter;

    public AtomicConsumer(AtomicCounter atomicCounter){
        this.atomicCounter = atomicCounter;
    }

    @Override
    public void run() {
        for(int j = 0; j < AtomicTest.LOOP; j++) {
            System.out.println("consumer : " + atomicCounter.getInteger());
            atomicCounter.decrement();
        }
    }
}

public class AtomicTest {

    final static int LOOP = 10000;

    public static void main(String[] args) throws InterruptedException {

        AtomicCounter counter = new AtomicCounter();
        AtomicProducer producer = new AtomicProducer(counter);
        AtomicConsumer consumer = new AtomicConsumer(counter);

        producer.start();
        consumer.start();

        producer.join();
        consumer.join();

        System.out.println(counter.getInteger());

    }
}

После тестирования видно, что сколько бы раз ни выполнялся цикл, конечный результат равен 0, то есть в случае многопоточного параллелизма использование AtomicInteger может обеспечить потокобезопасность. И incrementAndGet, и decrementAndGet являются атомарными операциями.

Недостатки оптимистической блокировки

У всего есть преимущества и недостатки, в индустрии программного обеспечения нет идеального решения, есть только оптимальное решение, поэтому оптимистическая блокировка также имеет свои недостатки и недостатки:

АВА-проблема

Проблема ABA заключается в том, что если значение переменной, прочитанной в первый раз, равно A, а когда она готова к записи в A, обнаруживается, что значение по-прежнему равно A, то в этом случае можно считать, что значение переменной равно A. значение A не изменилось. ? Это может быть случай A -> B -> A , но AtomicInteger так не думает, он просто верит в то, что видит, и видит то, что видит.

JDK 1.5 и вышеAtomicStampedReferenceкласс предоставляет эту возможность, в которойcompareAndSet 方法То есть сначала проверить, равна ли текущая ссылка ожидаемой ссылке, и равен ли текущий флаг ожидаемому флагу, и если все равны, атомарно установить ссылку и значение флага на заданное обновленное значение.

Вариант CAS, DCAS, также может быть использован для решения этой проблемы. DCAS — это маркер, указывающий количество модификаций, добавляющих ссылку на каждый файл V. Для каждого V этот счетчик увеличивается на 1, если ссылка изменяется один раз. Затем, когда переменную необходимо обновить, значение переменной и значение счетчика проверяются одновременно.

Накладные расходы на высокую петлю

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

CAS и синхронизированные сценарии использования

Короче говоря, CAS подходит для того, чтобы писать меньше (многократные сценарии чтения, как правило, меньше конфликтов), а синхронизированный подходит для того, чтобы писать больше (многократные сценарии записи, как правило, больше конфликтов).

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

Ресурс заблокирован, заблокирован ли поток

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

Поскольку в многопроцессорных средах некоторые ресурсы ограничены, иногда互斥访问(mutual exclusion)В настоящее время необходимо ввести понятие блокировки. Только поток, который получает блокировку, может получить доступ к ресурсу. Поскольку ядром многопоточности является квант времени ЦП, только один поток может одновременно получить блокировку. время. Тогда возникает проблема, что должен делать поток, который не получает блокировку?

Обычно существует два метода обработки: один заключается в том, что поток, который не получил блокировку, ожидает в цикле, чтобы определить, освободил ли ресурс блокировку.阻塞起来(NON-BLOCKING); Еще один способ справиться с этим — заблокировать себя и дождаться перепланированного запроса, который называется互斥锁.

Что такое спин-блокировка

Определение самоподъемника: когда нить пытается получить блокировку, если этот замок был получен (занят) кем-то другим, то этот поток не получит этот замок, нить будет подождать, интервал попробуйте снова после периода время. Этот тип замка цикла -> механизм ожидания называется自旋锁(spinlock).

Принцип блокировки вращения

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

Поскольку спин-блокировки позволяют избежать планирования процессов операционной системы и переключения потоков, спин-блокировки обычно подходят для коротких периодов времени. по этой причине,Ядро операционной системы часто использует спин-блокировки.. Однако при длительной блокировке спин-блокировка может быть очень дорогостоящей, поскольку она препятствует запуску и планированию других потоков. Чем дольше нить удерживает замок, тем нить, удерживающая замок, будетOS(Operating System)Чем выше риск сбоев планировщика. В случае прерывания другие потоки будут продолжать вращаться (неоднократно пытаясь получить блокировку), а поток, удерживающий блокировку, не собирается снимать блокировку, что приводит к неопределенной задержке, пока поток, удерживающий блокировку, не сможет завершить и освободить блокировку. Это.

Хороший способ решить описанную выше ситуацию — установить время вращения для блокировки вращения и снять блокировку вращения, как только время истечет. Назначение спин-блокировок состоит в том, чтобы занимать ресурсы ЦП, не освобождая их, и обрабатывать их сразу после получения блокировок. Но как выбрать время отжима? Если время выполнения вращения слишком велико, большое количество потоков будет находиться в состоянии вращения и занимать ресурсы ЦП, что повлияет на производительность всей системы. Поэтому выбор периода вращения имеет особое значение! В JDK 1.6 введены адаптивные спин-блокировки. Адаптивные спин-блокировки означают, что время вращения не фиксировано, а определяется предыдущим временем вращения на той же блокировке и состоянием блокировки. В основном считается, что время для контекста потока Переключение — оптимальное время.

Преимущества и недостатки спин-блокировок

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

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

Реализация спин-блокировки

Ниже мы используем код Java для реализации простой спин-блокировки.

public class SpinLockTest {

    private AtomicBoolean available = new AtomicBoolean(false);

    public void lock(){

        // 循环检测尝试获取锁
        while (!tryLock()){
            // doSomething...
        }

    }

    public boolean tryLock(){
        // 尝试获取锁,成功返回true,失败返回false
        return available.compareAndSet(false,true);
    }

    public void unLock(){
        if(!available.compareAndSet(true,false)){
            throw new RuntimeException("释放锁失败");
        }
    }

}

У этой простой спин-блокировки есть одна проблема:Многопоточность не может обеспечить честную конкуренцию. Для SpinlockTest выше, когда несколько потоков хотят получить блокировку, кто будет первымavailableустановить какfalseКто может получить блокировку первым, что может привести к тому, что некоторые потоки никогда не получат блокировку.线程饥饿. Точно так же, как мы ломились в столовую после уроков и толпились в метро после работы, мы обычно решаем такие проблемы с помощью очереди.QueuedSpinlock. Ученые-компьютерщики использовали различные способы реализации спин-блокировок с очередями, такие как TicketLock, MCSLock, CLHLock. Далее мы дадим общее представление об этих типах замков.

TicketLock

В информатике Ticketlock является механизм синхронизации или алгоритм блокировки, который является спинком, который используетticketдля управления порядком выполнения потоков.

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

Как и описанная выше система, TicketLock основан на механизме очереди «первым пришел – первым обслужен» (FIFO). Он повышает честность блокировки, а принципы его построения таковы: В TicketLock есть два значения типа int, начало равно 0, а первое значение равно队列ticket(队列票据), второе значение出队(票据). Билет очереди — это позиция потока в очереди, а билет удаления из очереди — это позиция в очереди билета, который теперь удерживает блокировку. Это может быть немного расплывчато, если говорить простыми словами,То есть билет очереди — это место, где вы получаете номер билета, а билет очереди — это место, где вы звоните по этому номеру.. Теперь должно быть яснее.

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

public class TicketLock {

    // 队列票据(当前排队号码)
    private AtomicInteger queueNum = new AtomicInteger();

    // 出队票据(当前需等待号码)
    private AtomicInteger dueueNum = new AtomicInteger();

    // 获取锁:如果获取成功,返回当前线程的排队号
    public int lock(){
        int currentTicketNum = dueueNum.incrementAndGet();
        while (currentTicketNum != queueNum.get()){
            // doSomething...
        }
        return currentTicketNum;
    }

    // 释放锁:传入当前排队的号码
    public void unLock(int ticketNum){
        queueNum.compareAndSet(ticketNum,ticketNum + 1);
    }

}

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

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

public class TicketLock2 {

    // 队列票据(当前排队号码)
    private AtomicInteger queueNum = new AtomicInteger();

    // 出队票据(当前需等待号码)
    private AtomicInteger dueueNum = new AtomicInteger();

    private ThreadLocal<Integer> ticketLocal = new ThreadLocal<>();

    public void lock(){
        int currentTicketNum = dueueNum.incrementAndGet();

        // 获取锁的时候,将当前线程的排队号保存起来
        ticketLocal.set(currentTicketNum);
        while (currentTicketNum != queueNum.get()){
            // doSomething...
        }
    }

    // 释放锁:从排队缓冲池中取
    public void unLock(){
        Integer currentTicket = ticketLocal.get();
        queueNum.compareAndSet(currentTicket,currentTicket + 1);
    }

}

На этот раз возвращаемое значение больше не нужно.При ведении бизнеса нужно кешировать текущий номер.После завершения дела нужно освободить кешированный тикет.

недостаток

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

Чтобы решить эту проблему, появились MCSLock и CLHLock.

CLHLock

Как упоминалось выше, TicketLock основан на очереди, затем CLHLock разработан на основе связанного списка.Изобретателями CLH являются: Craig, Landin и Hagersten, названные в честь их соответствующих букв. CLH — это расширяемая, высокопроизводительная и справедливая циклическая блокировка, основанная на связанных списках. Поток приложения может вращаться только для локальных переменных. Он будет постоянно опрашивать состояние прекурсора. Если он обнаружит, что прекурсор снял блокировку, он завершит вращение.

public class CLHLock {

    public static class CLHNode{
        private volatile boolean isLocked = true;
    }

    // 尾部节点
    private volatile CLHNode tail;
    private static final ThreadLocal<CLHNode> LOCAL = new ThreadLocal<>();
    private static final AtomicReferenceFieldUpdater<CLHLock,CLHNode> UPDATER =
            AtomicReferenceFieldUpdater.newUpdater(CLHLock.class,CLHNode.class,"tail");


    public void lock(){
        // 新建节点并将节点与当前线程保存起来
        CLHNode node = new CLHNode();
        LOCAL.set(node);

        // 将新建的节点设置为尾部节点,并返回旧的节点(原子操作),这里旧的节点实际上就是当前节点的前驱节点
        CLHNode preNode = UPDATER.getAndSet(this,node);
        if(preNode != null){
            // 前驱节点不为null表示当锁被其他线程占用,通过不断轮询判断前驱节点的锁标志位等待前驱节点释放锁
            while (preNode.isLocked){

            }
            preNode = null;
            LOCAL.set(node);
        }
        // 如果不存在前驱节点,表示该锁没有被其他线程占用,则当前线程获得锁
    }

    public void unlock() {
        // 获取当前线程对应的节点
        CLHNode node = LOCAL.get();
        // 如果tail节点等于node,则将tail节点更新为null,同时将node的lock状态职位false,表示当前线程释放了锁
        if (!UPDATER.compareAndSet(this, node, null)) {
            node.isLocked = false;
        }
        node = null;
    }
}

MCSLock

MCS Spinlock – это масштабируемая, высокопроизводительная и справедливая циклическая блокировка, основанная на связанных списках. Поток приложения использует только локальные переменные, а непосредственный предшественник отвечает за уведомление об окончании цикла, что значительно сокращает ненужные кэши процессора. синхронизаций снижает нагрузку на шину и память. MCS происходит от инициалов его изобретателей: Джона Меллора-Крамми и Майкла Скотта.

public class MCSLock {

    public static class MCSNode {
        volatile MCSNode next;
        volatile boolean isLocked = true;
    }

    private static final ThreadLocal<MCSNode> NODE = new ThreadLocal<>();

    // 队列
    @SuppressWarnings("unused")
    private volatile MCSNode queue;

    private static final AtomicReferenceFieldUpdater<MCSLock,MCSNode> UPDATE =
            AtomicReferenceFieldUpdater.newUpdater(MCSLock.class,MCSNode.class,"queue");


    public void lock(){
        // 创建节点并保存到ThreadLocal中
        MCSNode currentNode = new MCSNode();
        NODE.set(currentNode);

        // 将queue设置为当前节点,并且返回之前的节点
        MCSNode preNode = UPDATE.getAndSet(this, currentNode);
        if (preNode != null) {
            // 如果之前节点不为null,表示锁已经被其他线程持有
            preNode.next = currentNode;
            // 循环判断,直到当前节点的锁标志位为false
            while (currentNode.isLocked) {
            }
        }
    }

    public void unlock() {
        MCSNode currentNode = NODE.get();
        // next为null表示没有正在等待获取锁的线程
        if (currentNode.next == null) {
            // 更新状态并设置queue为null
            if (UPDATE.compareAndSet(this, currentNode, null)) {
                // 如果成功了,表示queue==currentNode,即当前节点后面没有节点了
                return;
            } else {
                // 如果不成功,表示queue!=currentNode,即当前节点后面多了一个节点,表示有线程在等待
                // 如果当前节点的后续节点为null,则需要等待其不为null(参考加锁方法)
                while (currentNode.next == null) {
                }
            }
        } else {
            // 如果不为null,表示有线程在等待获取锁,此时将等待线程对应的节点锁状态更新为false,同时将当前线程的后继节点设为null
            currentNode.next.isLocked = false;
            currentNode.next = null;
        }
    }
}

CLHLock и MCSLock

  • Все они основаны на связанных списках. Разница в том, что CLHLock основан на неявном связанном списке и не имеет реальных атрибутов последующих узлов. MCSLock — это отображаемый связанный список с атрибутом, указывающим на последующие узлы.
  • Состояние потока, получившего блокировку, сохраняется узлом (узлом), и каждый поток имеет независимый узел, что решает проблему синхронизации многопроцессорного кеша TicketLock.

Несколько потоков одновременно обращаются к ресурсам

Классификация состояний блокировки

Язык Java специально предназначенsynchronizedКлючевое слово устанавливает четыре состояния, а именно:Никаких замков, косых замков, легких замков и тяжелых замков, но прежде чем разбираться в этих блокировках, вам также необходимо понять заголовок объекта Java и Monitor.

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

Мы знаем, что synchronized — это пессимистическая блокировка. Вам нужно заблокировать ресурс, прежде чем операция будет синхронизирована. Эта блокировка находится в заголовке объекта, а что такое заголовок объекта Java? Возьмем в качестве примера виртуальную машину Hotspot.Заголовок объекта Hopspot в основном включает две части данных:Mark Word(标记字段)иclass Pointer(类型指针).

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

class Point: указатель объекта на его метаданные класса, виртуальная машина использует этот указатель, чтобы определить, экземпляром какого класса является объект.

Размер байтов, занимаемых Mark Word 32-битной виртуальной машины и 64-битной виртуальной машины, различен: Mark Word и класс Pointer 32-битной виртуальной машины занимают соответственно 32 бита байт, а Mark Word и класс Pointer 64-битной виртуальной машины. битовая виртуальная машина занимает 32 бита байтов соответственно Она занимает 64 бита байтов Давайте возьмем 32-битную виртуальную машину в качестве примера, чтобы увидеть, как распределяются байты ее Mark Word.

переведено на китайский

  • лицо без гражданства无锁Когда заголовок объекта используется для хранения хэш-кода объекта, 4 бита используются для хранения возраста генерации, 1 бит используется для хранения флага блокировки, а 2 бита используются для хранения флага блокировки как 01.
  • 偏向锁25-битное пространство зарезервировано, 23-битное используется для хранения идентификатора потока, 2-битное используется для хранения эпохи, 4-битное используется для хранения возраста поколения, 1-битный хранится независимо от смещения к идентификатору блокировки, 0 означает отсутствие блокировки, 1 означает смещенную блокировку, заблокировано флаг по-прежнему 01
  • 轻量级锁Непосредственно откройте 30-битное пространство для хранения указателя на запись блокировки в стеке, 2-битное хранилище хранит бит флага блокировки, а его бит флага равен 00.
  • 重量级锁Как и в случае облегченной блокировки, 30-битное пространство используется для хранения указателя на тяжеловесную блокировку, а 2-битное пространство используется для хранения идентификационного бита блокировки, который равен 11.
  • GC标记30-битное пространство памяти открыто, но не занято, а 2-битное пространство для хранения флага блокировки равно 11.

Биты флага блокировки блокировки без блокировки и блокировки со смещением равны 01, но первый 1 бит различает, является ли это состоянием без блокировки или состоянием блокировки со смещением.

О том, почему эта выделенная память, мы можемOpenJDKсерединаmarkOop.hppПеречисление в классе дает представление

объяснять

  • age_bits — это идентификатор, который мы называем рециркуляцией поколений, занимающий 4 байта.
  • lock_bits — бит флага блокировки, занимающий 2 байта
  • Biased_lock_bits — флаг необъективной блокировки, занимающий 1 байт
  • max_hash_bits - это количество байтов, занятых HASHCODE для бесплатного расчета блокировки. Если это 32-битная виртуальная машина, она составляет 32 - 4 - 2 -1 = 25 байтов. Если это 64-битная виртуальная машина, 64 - 4 - 2 - 1 = 57 байт, но будет неиспользовано 25 байт, поэтому 64-битный хэшкод занимает 31 байта
  • hash_bits для 64-битных виртуальных машин, если максимальное количество байт больше 31, берем 31, иначе берем реальное количество байт
  • cms_bits Я думаю, он должен занимать 0 байт, если это 64-битная виртуальная машина, или 1 байт, если она 64-битная.
  • epoch_bits — размер байтов, занятых эпохой, 2 байта.

Синхронизированный замок

synchronizedИспользуемый замок блокировки хранится в заголовке объекта Java.

JVM реализует синхронизацию методов и синхронизацию блоков кода на основе входа и выхода из объектов Monitor. Синхронизация блока кода реализована с помощью директив monitorenter и monitorexit, директива monitorenter вставляется в начало синхронизированного блока после компиляции, а monitorexit вставляется в конец метода и в исключение. С любым объектом связан монитор, и когда монитор удерживается, он блокируется.

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

Monitor

Синхронизация реализуется через блокировку монитора (монитора) внутри объекта Суть блокировки монитора реализуется за счет опоры на блокировку взаимного исключения (Mutex Lock) базовой операционной системы. Операционная система должна переключаться из состояния пользователя в состояние ядра для переключения между потоками.Эта стоимость очень высока, а переход между состояниями занимает относительно много времени, поэтому Synchronized неэффективен. Поэтому такой тип блокировки, зависящий от реализации операционной системы, называется Mutex Lock.重量级锁.

В Java SE 1.6 введены затраты производительности на получение и снятие блокировок.偏向锁и轻量级锁: всего существует 4 состояния блокировки, а уровни от низкого до высокого: состояние без блокировки, состояние смещенной блокировки, облегченное состояние блокировки и тяжелое состояние блокировки. Замки можно улучшать, но нельзя понижать.

Таким образом, всего существует четыре состояния блокировки: состояние без блокировки, блокировка со смещением, облегченная блокировка и блокировка с большим весом. С конкуренцией замков замки могут быть модернизированы со смещенных замков до легких замков, а затем модернизированы до тяжелых замков (но модернизация замков является односторонней, то есть ее можно модернизировать только от низкого до высокого, и там не будет замков даунгрейда). Предвзятые блокировки и облегченные блокировки включены по умолчанию в JDK 1.6. Мы также можем отключить предвзятые блокировки с помощью -XX:-UseBiasedLocking=false.

Классификация замков и их пояснения

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

нет замка

无锁状态, lock-free означает, что ресурс не заблокирован, все потоки могут получить доступ к одному и тому же ресурсу, но только один поток может успешно изменить ресурс.

Особенность lock-free заключается в том, что операция модификации выполняется в цикле.Поток будет продолжать попытки изменить общий ресурс, пока не сможет успешно изменить ресурс и выйти.В процессе нет конфликта, что очень похоже CAS мы представили в предыдущей статье Реализация, принцип и применение CAS - это реализация без блокировки. Никакой замок не может полностью заменить замок, но эффективность блокировки в некоторых случаях очень высока.

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

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

Как видно из выделения заголовка объекта, предвзятые блокировки — это гораздо больше, чем безблокировочные.线程IDиepoch, опишем процесс получения смещенных замков

Процесс получения предвзятой блокировки

  1. Во-первых, поток обращается к блоку синхронизированного кода, проверяя заголовок объекта.锁标志位Определяем текущее состояние блокировки, если оно равно 01, что указывает на отсутствие блокировки или смещенную блокировку, то в соответствии с是否偏向锁Отметка о том, является ли это блокировкой без блокировки или блокировкой со смещением, если она без блокировки, перейдите к следующему шагу.
  2. Поток использует операцию CAS, чтобы попытаться заблокировать объект.Если CAS используется для успешной замены ThreadID, это означает, что это первая блокировка, тогда текущий поток получит предвзятую блокировку объекта, а Текущий поток будет записан в Mark Word заголовка объекта.Такая информация, как идентификатор потока и эпоха времени, когда была получена блокировка, а затем выполняется блок синхронизированного кода.

Глобальная безопасная точка (Safe Point): понимание глобальной безопасной точки потребует некоторых знаний в нижней части языка C. Здесь простое понимание безопасной точки — это место, где поток в коде Java может приостановить выполнение.

Подождите, пока в следующий раз поток не войдет и не выйдет из синхронизированного блока кода, и ему не нужноCASЧтобы заблокировать и разблокировать операцию, вам нужно просто судить, только ли идентификатор темы, указывающий на текущий поток, хранятся в Mark Word заголовка объекта. Конечно, судливый флаг оценивается в соответствии с флагом блокировки блокировки. Если он представлен блок-схемой, это выглядит следующим образом

Закрыть смещенную блокировку

Блокировка смещения используется по умолчанию в Java 6 и Java 7.启用из. Поскольку предвзятая блокировка предназначена для повышения производительности, когда только один поток выполняет синхронизированный блок, если вы уверены, что все блокировки в вашем приложении обычно конфликтуют, вы можете отключить предвзятую блокировку с помощью параметра JVM:-XX:-UseBiasedLocking=false, то по умолчанию программа перейдет в состояние облегченной блокировки.

Об эпохах

Один из заголовков объекта с блокировкой называетсяepochЗначение , которое служит отметкой времени для достоверности отклонения.

Легкий замок

轻量级锁Это означает, что когда текущая блокировка является смещенной блокировкой, а к ресурсу обращается другой поток, смещенная блокировка будет обновлена ​​до轻量级锁, другие потоки будут проходить自旋При попытке получить блокировку она не блокируется, что повышает производительность.Ниже приведен подробный процесс получения.

Легкий процесс блокировки замка

  1. Сразу после предыдущего шага, если операция CAS по замене ThreadID не удалась, переходим к следующему шагу
  2. Если операции CAS не удается заменить ThreadID (в это время переключиться на перспективу другого потока), это означает, что к ресурсу был осуществлен синхронный доступ. В это время будет выполнена операция отзыва блокировки, смещенная блокировка будет отменена, и затем дождитесь оригинального держателя косого замка.全局安全点(SafePoint)Когда первоначальные держатели приостановят смещенную запирающую нить, а затем проверит состояние первоначальных держателей, как правило, блокирующих, если вы прекратите синхронизацию, она будет стремиться разбудить нить, удерживающую блокировку, следующий шаг.
  3. Проверить, является ли запись Mark Word в заголовке объекта идентификатором текущего потока, если да, выполнить синхронный код, если нет, выполнитьПроцесс получения предвзятой блокировкишага 2.

Если его представить процессом, то он выглядит следующим образом (вместе с приобретением смещенных замков)

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

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

Процесс приобретения в тяжелом весе

  1. Затем описанный выше процесс получения смещенной блокировки обновляется с смещенной блокировки до облегченной блокировки, и выполняется следующий шаг.

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

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

    • Определить, указывает ли указатель блокировки записи в слове метки в заголовке объекта на указатель записи в текущем стеке.

  • Соответствует ли информация слова отметки, скопированная в текущей записи блокировки потока, слову отметки в заголовке объекта.

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

  1. Выделите запись блокировки в стеке текущего потока, скопируйте MarkWord в заголовке объекта в запись блокировки текущего потока и выполните операцию блокировки CAS, которая укажет указатель записи блокировки в заголовке объекта Mark Word на Текущая запись блокировки потока. В случае успеха получить запись блокировки текущего потока. Облегченная блокировка, выполнить синхронный код, затем перейти к шагу 3, в случае неудачи перейти к следующему шагу

  2. Если текущий поток не использует CAS для успешного получения блокировки, он некоторое время будет вращаться и попытается получить ее снова.Если блокировка не была получена после того, как несколько циклов достигли верхнего предела, облегченная блокировка будет обновлена ​​до重量级锁

Если это представлено в виде блок-схемы, подобной этой

Справедливость и несправедливость замков

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

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

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

Тогда мы можем сделать следующие выводы на основе приведенного выше описания.

честный замокУказывает, что порядок, в котором потоки получают блокировки, распределяется в соответствии с порядком, в котором потоки блокируются, то есть в порядке FIFO «первым пришел — первым обслужен». инесправедливый замокЭто механизм упреждающего захвата блокировок. Он захватывает блокировки случайным образом. В отличие от честных блокировок, те, кто первым пришел, не обязательно получают блокировки первыми. Этот метод может привести к тому, что некоторые потоки никогда не получат блокировки, и результат будет несправедливым.

Реализация блокировки справедливости

В Java мы обычно передаемReetrantLockЧтобы добиться справедливости блокировки

Давайте объясним честность и несправедливость блокировок на двух примерах.

Блокировка справедливости

public class MyFairLock extends Thread{

    private ReentrantLock lock = new ReentrantLock(true);
    public void fairLock(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()  + "正在持有锁");
        }finally {
            System.out.println(Thread.currentThread().getName()  + "释放了锁");
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        MyFairLock myFairLock = new MyFairLock();
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + "启动");
            myFairLock.fairLock();
        };
        Thread[] thread = new Thread[10];
        for(int i = 0;i < 10;i++){
            thread[i] = new Thread(runnable);
        }
        for(int i = 0;i < 10;i++){
            thread[i].start();
        }
    }
}

Мы создали ReetrantLock и передали конструктору значение true, мы можем просмотреть конструктор ReetrantLock.

public ReentrantLock(boolean fair) {
  sync = fair ? new FairSync() : new NonfairSync();
}

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

И Sync — это внутренний класс в ReentrantLock, Sync наследуетAbstractQueuedSynchronizerКласс AbstractQueuedSynchronizer — это то, что мы часто называем AQS, который является наиболее важным классом в JUC (java.util.concurrent), посредством которого реализуются эксклюзивные блокировки и общие блокировки.

abstract static class Sync extends AbstractQueuedSynchronizer {...}

То есть после того, как мы установили для параметра Fair значение true, мы можем реализовать честную блокировку, верно? Вернемся к примеру кода, мы можем выполнить этот код, его вывод получается последовательно (из соображений нехватки места он не будет здесь размещаться), то есть мы создали честную блокировку

Блокировка несправедливости

Противоположностью справедливости является несправедливость, мы устанавливаемfairЕсли параметр равен true, реализуется честная блокировка, и наоборот, если мы установим для параметра Fair значение false, будет ли это несправедливая блокировка? докажи это фактами

private ReentrantLock lock = new ReentrantLock(false);

Другой код остается без изменений, давайте выполним его и посмотрим вывод (частичный вывод)

Thread-1启动
Thread-4启动
Thread-1正在持有锁
Thread-1释放了锁
Thread-5启动
Thread-6启动
Thread-3启动
Thread-7启动
Thread-2启动

Видно, что запуск потока происходит не по порядку.Видно, что получение блокировки недобросовестной блокировкой происходит не по порядку, то есть идет процесс вытеснения блокировки. То есть мы реализуем несправедливую блокировку, установив для параметра fair значение false.

Базовый обзор ReentrantLock

ReentrantLock — это可重入锁, также互斥锁, который имеет то же самоеsynchronizedТот же метод и семантика блокировки монитора, но он имеет больше расширяемых возможностей, чем синхронизированный.

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

Конструктор этого класса принимает необязательный параметр справедливости.Если для справедливости задано значение true, когда несколько потоков соревнуются за блокировку, доступ к блокировке, как правило, получает поток с наибольшим временем ожидания, что также является проявлением справедливости. В противном случае блокировка не может гарантировать порядок доступа каждого потока, то есть нечестная блокировка. Программы, использующие справедливые блокировки, к которым обращаются многие потоки, могут показывать较低(т.е. медленнее; обычно намного медленнее). Но количество раз, когда блокировка получается и поток гарантированно не голодает, относительно невелико. В любом случае обратите внимание: справедливость блокировок не гарантирует справедливость планирования потоков. Таким образом, один из многопоточных потоков, использующих справедливую блокировку, может получить ее несколько раз подряд, в то время как другие активные потоки не выполняют и в настоящее время удерживают блокировку. Это тоже互斥性проявление.

также обратите вниманиеtryLock()Метод не поддерживает справедливость. Если блокировка получена, даже если ее ждут другие потоки, она все равно может успешно вернуться.

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

class MyFairLock {
  private final ReentrantLock lock = new ReentrantLock();

  public void m() {
    lock.lock();  
    try {
      // ... 
    } finally {
      lock.unlock()
    }
  }
}

Блокировка ReentrantLock поддерживает до 2 147 483 647 рекурсивных блокировок в одном потоке. Попытка превысить этот предел приведет к тому, что метод блокировки выдаст ошибку.

Как ReentrantLock обеспечивает справедливость блокировки

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

Отслеживайте обнаружение исходного кода, звонитеLock.lock()На самом деле метод называетсяsyncвнутренний метод

abstract void lock();

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

lock — это абстрактный метод, который должен быть реализован подклассами, а классы, наследующие AQS, в основном включают

Мы видим, что все классы, реализующие AQS, находятся в пакете JUC, и их пять основных категорий:ReentrantLock,ReentrantReadWriteLock,Semaphore,CountDownLatchиThreadPoolExecutor, среди которых ReentrantLock, ReentrantReadWriteLock и Semaphore могут реализовывать справедливые и нечестные блокировки.

Ниже честный замокFairSyncотношения наследования

несправедливый замокNonFairSyncотношения наследования

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

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

hasQueuedPredecessors() также является методом в AQS, он в основном используется дляЗапрос, если какой-либо поток ожидал получения блокировки дольше, чем текущий поток, то есть каждый ожидающий поток находится в очереди. Этот метод должен определить, существует ли очередь, ожидающая блокировки дольше, чем она сама, когда текущий поток получает блокировку. Если перед текущим потоком есть поток в очереди, return true, возвращает false, если текущий поток находится в начале очереди или очередь пуста.

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

Различать в зависимости от того, является ли блокировка повторной

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

Блокировка с повторным входом, также известная как рекурсивная блокировка, означает, что когда тот же поток получает блокировку во внешнем методе, внутренний метод, который входит в поток, автоматически получает блокировку (при условии, что объект блокировки должен быть тем же объектом или классом). ), не заблокируется, потому что он был получен ранее и не был выпущен. на ЯвеReentrantLockиsynchronizedВсе они являются блокировками с повторным входом. Одно из преимуществ блокировок с повторным входом состоит в том, что в определенной степени можно избежать взаимоблокировок.

Давайте сначала посмотрим на фрагмент кода, чтобы проиллюстрировать повторный вход в синхронизированный

private synchronized void doSomething(){
  System.out.println("doSomething...");
  doSomethingElse();
}

private synchronized void doSomethingElse(){
  System.out.println("doSomethingElse...");
}

В приведенном выше коде мы имеемdoSomething()иdoSomethingElse()используется отдельноsynchronizedДля блокировки в методе doSomething() вызывается метод doSomethingElse().Поскольку синхронизация является повторно вводимой блокировкой, тот же поток может также ввести метод doSomethingElse() при вызове метода doSomething().

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

Если synchronized — нереентерабельная блокировка, то при вызове метода doSomethingElse() блокировка doSomething() должна быть потеряна, ведь блокировка объекта уже удерживается текущим потоком и не может быть снята. Так что в это время будет тупик.

То есть нереентерабельные блокировки могут вызывать взаимоблокировки.

Несколько потоков могут использовать одну и ту же блокировку

Эксклюзивные блокировки и общие блокировки

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

Эксклюзивная блокировка, также известная как монопольная блокировка, означает, что блокировка может одновременно принадлежать только одному потоку, и другие потоки будут заблокированы, если они захотят получить доступ к ресурсу. Классом реализации synchronized в JDK и Lock в JUC является мьютекс.

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

Мы видим, что ReentrantReadWriteLock имеет две блокировки:ReadLockиWriteLock, то есть блокировку чтения и блокировку записи, которые вместе называются блокировками чтения-записи. Дальнейшее наблюдение показывает, что ReadLock и WriteLock — это блокировки, реализованные внутренним классом Sync. Синхронизация унаследована от подкласса AQS. AQS является основой параллелизма. Эта структура также существует в CountDownLatch, ReentrantLock и Semaphore.

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

Добро пожаловать, чтобы следовать

Ссылка на статью:

Пессимистическая блокировка и оптимистичная блокировка многопоточности Java

baike.baidu.com/item/пессимистический замок

blog.CSDN.net/QQ_34337272…

Woohoo.блог Java.net/Jinfeng_Wan…

blog.hufeifei.cn/Статья о спин-блокировках

En. Wikipedia.org/wiki/ticket…

blog.Stephen clearnight.com/2013/04/zealous…

исследователь.Watson.IBM.com/researcher/…

Специальности.Meituan.com/2018/11/15/…

Woo Woo Краткое описание.com/Afraid/Hungry Hungry 337 From 5…

blog.CSDN.net/о Чанг Вэнь/Ах…