Глубокое понимание принципа реализации ReentrantLock

Java
image

Введение в ReentrantLock

ReentrantLockдаJavaсуществуетJDK1.5Введенная явная блокировка отличается от встроенной блокировки (синхронизированной) по принципу реализации и функции, и в конце статьи мы сравним две блокировки.
Сначала нам нужно знатьReentrantLockоснован наAQSдостигнуто, поэтому мы должны быть правыAQSЧем лучше вы знаете, тем лучше вы можете учитьсяReentrantLockAQSДля ознакомления вы можете обратиться к статье, которую я написал ранее«Одна статья поможет вам быстро освоить AQS», вот краткий обзорAQS.

Обзор AQS

AQSкоторыйAbstractQueuedSynchronizerАббревиатура , это абстрактный класс, который реализует две очереди внутри, а именноочередь синхронизациииусловная очередь. вочередь синхронизацииЭто двусвязный список, в котором потоки хранятся в состоянии ожидания, очереди для пробуждения для получения блокировки иусловная очередьЭто односвязный список, в котором потоки хранятся в состоянии ожидания, но результатом пробуждения этих потоков является то, что они добавляются в конец очереди синхронизации.AQSЧто он делает, так это управляет связью между потоками в двух очередях.состояние ожидания - проснутьсяРабота.
В очереди синхронизации также есть2средний режим соответственноэксклюзивный режимиобщий режим, разница между двумя режимами в том, чтоAQSПроходить ли пробуждение при пробуждении узла потока, эти два режима соответствуютэксклюзивный замокиобщий замок.
AQSЭто абстрактный класс, поэтому его нельзя создать напрямую. Когда нам нужно реализовать собственную блокировку, мы можем ее наследовать.AQSзатем переписатьКак получить замокиКак снять блокировкуиуправлять состояниемReentrantLockпутем переписыванияAQSизtryAcquireиtryReleaseметод реализованlockиunlock.

Принцип ReentrantLock

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

Структура ReentrantLock

во-первыхReentrantLockунаследован от родительского классаLock, а затем иметь3внутренний класс, гдеSyncВнутренние классы наследуются отAQS, два других внутренних класса наследуются отSync, эти два класса используются дляЧестные и нечестные замкииз.
пройти черезSyncПереопределенный методtryAcquire,tryReleaseЗнать,ReentrantLockдостигнутоAQSЭксклюзивный режим, то есть исключительная блокировка, эта блокировка является пессимистической блокировкой..

ReentrantLockСуществует важная переменная-член:

private final Sync sync;

Эта переменная используется для указания наSyncподкласс , то естьFairSyncилиNonfairSync, который является полиморфнымСсылка родительского класса указывает на дочерний класс,конкретныйSycnНа какой подкласс указывать, смотрите конструктор:

public ReentrantLock() {
    sync = new NonfairSync();
}

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

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

Принцип реализации несправедливой блокировки

Когда мы используем конструктор без аргументов для построенияReentrantLock lock = new ReentrantLock(), что создает несправедливую блокировку.

public ReentrantLock() {
    sync = new NonfairSync();
}

//或者传入false参数 创建的也是非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

метод блокировки получает блокировку

  1. lockвызов методаCASнастройки методаstateзначение, еслиstateравно ожидаемому значению0(представляющий, что замок не занят), тоstateобновить до1(от имени потока успешно получает блокировку), затем выполнитеsetExclusiveOwnerThreadМетод напрямую устанавливает поток в качестве владельца блокировки. еслиCASнастраиватьstateзначение терпит неудачу, т.е.stateне равно0, что означает, что замок занят, затем выполнитеacquire(1), следующие шаги.
  2. nonfairTryAcquireметод вызывается первымgetStateПриобретение методаstateзначение, еслиstateзначение0(поток, который ранее занимал блокировку, только что освободил блокировку), затем используйтеCASЭтоstateЗначение , если установка выполнена успешно, поток устанавливается как владелец блокировки и возвращаетсяtrue. еслиstateЗначение не0, тогдаперечислитьgetExclusiveOwnerThreadМетод проверки того, является ли поток, занимающий блокировку, самим собой, если да, то напрямуюstate + 1, затем вернутьсяtrue. еслиstateне для0И хозяин замка не сам, то верниfalse,Затем поток войдет в очередь синхронизации.

final void lock() {
    //CAS操作设置state的值
    if (compareAndSetState(0, 1))
        //设置成功 直接将锁的所有者设置为当前线程 流程结束
        setExclusiveOwnerThread(Thread.currentThread());
    else
        //设置失败 则进行后续的加入同步队列准备
        acquire(1);
}

public final void acquire(int arg) {
    //调用子类重写的tryAcquire方法 如果tryAcquire方法返回false 那么线程就会进入同步队列
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

//子类重写的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
    //调用nonfairTryAcquire方法
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //如果状态state=0,即在这段时间内 锁的所有者把锁释放了 那么这里state就为0
    if (c == 0) {
        //使用CAS操作设置state的值
        if (compareAndSetState(0, acquires)) {
            //操作成功 则将锁的所有者设置成当前线程 且返回true,也就是当前线程不会进入同步
            //队列。
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果状态state不等于0,也就是有线程正在占用锁,那么先检查一下这个线程是不是自己
    else if (current == getExclusiveOwnerThread()) {
        //如果线程就是自己了,那么直接将state+1,返回true,不需要再获取锁 因为锁就在自己
        //身上了。
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //如果state不等于0,且锁的所有者又不是自己,那么线程就会进入到同步队列。
    return false;
}

снять блокировку tryRelease

  1. Определить, является ли текущий поток владельцем блокировки, и если да, то перейти к шагам2, и выдает исключение, если нет.
  2. Убедившись, что на этот раз блокировка снятаstateЯвляется ли значение 0, если да, то оно представляетЯвляется ли блокировка повторно используемой?, затем установите владельца блокировкиnullи вернутьсяtrue, затем выполните шаги3, если нет тоУказывает, что блокировка была повторно введенаШаги4.
  3. Теперь блокировка снята, т.е.state=0, разбудите узел-преемник в очереди синхронизации, чтобы получить блокировку.
  4. Блокировка еще не снята, т.е.state!=0, не будите очередь синхронизации.

public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
    //子类重写的tryRelease方法,需要等锁的state=0,即tryRelease返回true的时候,才会去唤醒其
    //它线程进行尝试获取锁。
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
    
protected final boolean tryRelease(int releases) {
    //状态的state减去releases
    int c = getState() - releases;
    //判断锁的所有者是不是该线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        //如果所的所有者不是该线程 则抛出异常 也就是锁释放的前提是线程拥有这个锁,
        throw new IllegalMonitorStateException();
    boolean free = false;
    //如果该线程释放锁之后 状态state=0,即锁没有重入,那么直接将将锁的所有者设置成null
    //并且返回true,即代表可以唤醒其他线程去获取锁了。如果该线程释放锁之后state不等于0,
    //那么代表锁重入了,返回false,代表锁还未正在释放,不用去唤醒其他线程。
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

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

метод блокировки получает блокировку

  1. получить статусstateзначение, еслиstate=0Это означает, что блокировка не занята другими потоками (но это не означает, что в очереди на синхронизацию нет ни одного потока), а этапы выполнения2. еслиstate!=0Это означает, что блокировка занята другими потоками, выполните шаги3.
  2. Определить, есть ли поток (узел) в очереди синхронизации, если нет, напрямую установить владельца блокировки на текущий поток, обновить состояние, а затем вернуть true.
  3. Определить, является ли владельцем блокировки текущий поток, если да, то обновить значение состояния state, а затем вернуть true, если нет, вернуть false, то есть поток будет добавлен в очередь синхронизации

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

final void lock() {
    acquire(1);
}

public final void acquire(int arg) {
    //同步队列中有线程 且 锁的所有者不是当前线程那么将线程加入到同步队列的尾部,
    //保证了公平性,也就是先来的线程先获得锁,后来的不能抢先获取。
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //判断状态state是否等于0,等于0代表锁没有被占用,不等于0则代表锁被占用着。
    if (c == 0) {
        //调用hasQueuedPredecessors方法判断同步队列中是否有线程在等待,如果同步队列中没有
        //线程在等待 则当前线程成为锁的所有者,如果同步队列中有线程在等待,则继续往下执行
        //这个机制就是公平锁的机制,也就是先让先来的线程获取锁,后来的不能抢先获取。
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //判断当前线程是否为锁的所有者,如果是,那么直接更新状态state,然后返回true。
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //如果同步队列中有线程存在 且 锁的所有者不是当前线程,则返回false。
    return false;
}

снять блокировку tryRelease

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

lockInterruptibility получает блокировку прерываемым способом

ReentrantLockотносительноSynchronizedИмеет несколько более удобных функций, таких как прерываемый способ получения замков.

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    //如果当前线程已经中断了,那么抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //如果当前线程仍然未成功获取锁,则调用doAcquireInterruptibly方法,这个方法和
    //acquireQueued方法没什么区别,就是线程在等待状态的过程中,如果线程被中断,线程会
    //抛出异常。
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

Метод ожидания timeout tryLock для получения блокировки

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

  1. Определить, был ли прерван текущий узел, сгенерировать исключение, если он был прерван, попытаться получить блокировку, если он не был прерван, и вызвать, если получение не удалосьdoAcquireNanosМетод получает блокировку, ожидая тайм-аута.
  2. Узел, который инкапсулирует текущий узел в монопольном режиме, добавляется в конец очереди синхронизации.
  3. в «мёртвую петлю»,Но у этого бесконечного цикла есть предел, то есть, когда поток достигнет времени тайм-аута и еще не получил блокировку, он вернетсяfalse, конец цикла. Вот звонокLockSupport.parkNanosметод, не прерывается в течение периода ожидания, то поток начнется сСостояние ожидания тайм-аута изменилось на состояние готовности, а затем поCPUРасписание продолжает выполнять цикл,В это время поток достиг времени ожидания и возвращает false.

LockSuportметод может ответитьThread.interrupt, но исключение не выдается

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    //如果当前线程已经中断了  则抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //再尝试获取一次 如果不成功则调用doAcquireNanos方法进行超时等待获取锁
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    //计算超时的时间 即当前虚拟机的时间+设置的超时时间
    final long deadline = System.nanoTime() + nanosTimeout;
    //调用addWaiter将当前线程封装成独占模式的节点 并且加入到同步队列尾部
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            //如果当前节点的前驱节点为头结点 则让当前节点去尝试获取锁。
            if (p == head && tryAcquire(arg)) {
                //当前节点获取锁成功 则将当前节点设置为头结点,然后返回true。
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            //如果当前节点的前驱节点不是头结点 或者 当前节点获取锁失败,
            //则再次判断当前线程是否已经超时。
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            //调用shouldParkAfterFailedAcquire方法,告诉当前节点的前驱节点 我要进入
            //等待状态了,到我了记得喊我,即做好进入等待状态前的准备。
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                //调用LockSupport.parkNanos方法,将当前线程设置成超时等待的状态。
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

Механизм ожидания/уведомления ReentrantLock

мы знаем ключевые словаSynchronized + Objectизwaitиnotify,notifyAllметод может достичьждать/уведомитьмеханизм, тоReentrantLockМожно ли реализовать такой механизм ожидания/уведомления, ответ: да.
ReentrantLockпройти черезConditionобъект, то естьусловная очередьпонял иwait,notify,notifyAllта же семантика. выполнение потокаcondition.await()способ перевода узла 1 из очереди синхронизации в очередь условий.

выполнение потокаcondition.signal()способ перевода узла 1 из условной очереди в очередь синхронизации.

Поскольку только потоки в очереди синхронизации могут получить блокировку, передайтеConditionобъектwaitиsignalМетоды могут реализовывать механизм ожидания/уведомления.
Пример кода:

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void await() {
    lock.lock();
    try {
        System.out.println("线程获取锁----" + Thread.currentThread().getName());
        condition.await(); //调用await()方法 会释放锁,和Object.wait()效果一样。
        System.out.println("线程被唤醒----" + Thread.currentThread().getName());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
        System.out.println("线程释放锁----" + Thread.currentThread().getName());
    }
}

public void signal() {
    try {
        Thread.sleep(1000);  //休眠1秒钟 等等一个线程先执行
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    lock.lock();
    try {
        System.out.println("另外一个线程获取到锁----" + Thread.currentThread().getName());
        condition.signal();
        System.out.println("唤醒线程----" + Thread.currentThread().getName());
    } finally {
        lock.unlock();
        System.out.println("另外一个线程释放锁----" + Thread.currentThread().getName());
    }
}

public static void main(String[] args) {
    Test t = new Test();
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            t.await();
        }
    });

    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            t.signal();
        }
    });

    t1.start();
    t2.start();
}

Рабочий вывод:

线程获取锁----Thread-0
另外一个线程获取到锁----Thread-1
唤醒线程----Thread-1
另外一个线程释放锁----Thread-1
线程被唤醒----Thread-0
线程释放锁----Thread-0

Процесс выполнения, вероятно, таков, потокt1Сначала получите блокировку, выведите «Поток получает блокировку ---- Поток-0», затем потокt1перечислитьawaitметод, результатом вызова этого метода являетсянитьt1Снимите блокировку и войдите в состояние ожидания, ожидая пробуждения, следующая веткаt2Блокировка была получена, и было выведено «другой поток получил блокировку ---- Thread-1», а потокt2перечислитьsignalметод, результатом вызова этого метода являетсяРазбудите поток в очереди условий (Condition), затем потокt1пробуждается, и на этот раз нитьt2не освобождает замок, нитьt1Нет возможности получить блокировку, ждите тредаt2Продолжить выполнение потока после вывода «пробуждать поток----Thread-1»t2Снимите блокировку и выведите «другой поток снимает блокировку ---- Thread-1», в это время потокt1Получите блокировку, продолжайте выполнять вывод вниз线程被唤醒----Thread-0, а затем снимите блокировку вывода "Thread Release Lock----Thread-0".

Что делать, если вы хотите разбудить некоторые потоки по отдельности? В этом случае необходимо использовать несколькоConditionобъект, потому чтоReentrantLockПоддержка создания несколькихConditionобъект, например:

//为了减少篇幅 仅给出伪代码
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Condition condition1 = lock.newCondition();

//线程1 调用condition.await() 线程进入到条件队列
condition.await();

//线程2 调用condition1.await() 线程进入到条件队列
condition1.await();

//线程32 调用condition.signal() 仅唤醒调用condition中的线程,不会影响到调用condition1。
condition1.await();

Таким образом реализуется функция частичного пробуждения.

Сравнение ReentrantLock и Synchronized

оSynchronizedВведение можно посмотреть«Использование синхронизированного (1)»,«Углубленный анализ принципа синхронизации и процесса расширения замка (2)»

ReentrantLock Synchronized
низкоуровневая реализация пройти черезAQSвыполнить пройти черезJVMпонял, гдеsynchronizedСуществует также несколько типов замков, в дополнение к тяжеловесным замкам, которыеmonitorВ дополнение к реализации объекта (примитив взаимного исключения взаимного исключения мьютекса операционной системы) другие типы реализуются через заголовок объекта.
Это реентерабельный да да
честный замок да нет
несправедливый замок да да
тип замка Пессимистическая блокировка, явная блокировка Пессимистическая блокировка, неявная блокировка (встроенная блокировка)
Поддерживать ли прерывание да нет
Поддерживать ли ожидание тайм-аута да нет
Автоматически получать/освобождать блокировки нет да

Ссылаться на

Искусство параллельного программирования на Java
Глубокое понимание AbstractQueuedSynchronizer (AQS)
Анализ принципа реентерабельной блокировки Java ReentrantLock)

Оригинальный адрес:В ожидании твоего таланта/2019/03/24/…