>  기사  >  Java  >  Java의 재진입 잠금 원리를 설명하는 자세한 샘플 코드

Java의 재진입 잠금 원리를 설명하는 자세한 샘플 코드

黄舟
黄舟원래의
2017-03-22 11:07:291784검색

1. 개요

이 글에서는 먼저 Lock 인터페이스, ReentrantLock의 클래스 계층 구조, 잠금 함수 템플릿 클래스 AbstractQueuedSynchronizer의 간단한 원리를 소개하고 잠금을 분석하여 ReentrantLock의 내부 원리를 설명합니다. ReentrantLock 의 메소드 및 잠금 해제 메소드를 확인하고 마지막으로 요약합니다. 이 문서에서는 ReentrantLock의 조건 변수를 다루지 않습니다.

1.1. 잠금 인터페이스

잠금 인터페이스는 동시성 제어 도구를 추상화한 것입니다. 동기화된 키워드를 사용하는 것보다 더 유연하며 조건 변수를 지원할 수 있습니다. 동시성을 제어하는 ​​도구입니다. 일반적으로 특정 공유 리소스의 독점성을 제어합니다. 즉, 단 하나의 스레드만이 이 잠금을 획득하고 동시에 리소스를 점유할 수 있습니다. 다른 스레드가 잠금을 획득하려면 이 스레드가 잠금을 해제할 때까지 기다려야 합니다. Java 구현의 ReentrantLock은 이러한 잠금입니다. 여러 스레드가 리소스를 읽을 수 있도록 허용하지만 하나의 스레드만 리소스 쓰기를 허용할 수 있는 또 다른 종류의 잠금입니다. ReadWriteLock은 읽기-쓰기 잠금이라고 하는 특수 잠금입니다. 다음은 Lock 인터페이스의 여러 메서드에 대한 전반적인 설명입니다.

설명
메서드 이름
잠금 잠금을 획득합니다. 잠금을 획득할 수 없는 경우 잠금이 획득될 때까지 현재 스레드를 예약할 수 없습니다.
lockInterruptically 현재 스레드가 중단되지 않는 한 잠금을 획득합니다. 잠금을 획득한 경우 즉시 반환합니다. 획득할 수 없는 경우 현재 스레드는 다음 두 가지 상황이 발생할 때까지 대기 상태가 됩니다.
方法名称 描述
lock 获取锁,如果锁无法获取,那么当前的线程就变为不可被调度,直到锁被获取到
lockInterruptibly 获取锁,除非当前线程被中断。如果获取到了锁,那么立即返回,如果获取不到,那么当前线程变得不可被调度,一直休眠直到下面两件事情发生:

1、当前线程获取到了锁

2、其他的线程中断了当前的线程

tryLock 如果调用的时候能够获取锁,那么就获取锁并且返回true,如果当前的锁无法获取到,那么这个方法会立刻返回false
tryLcok(long time,TimeUnit unit) 在指定时间内尝试获取锁如果可以获取锁,那么获取锁并且返回true,如果当前的锁无法获取,那么当前的线程变得不可被调度,直到下面三件事之一发生:

1、当前线程获取到了锁

2、当前线程被其他线程中断

3、指定的等待时间到了

 

unlock 释放当前线程占用的锁
newCondition 返回一个与当前的锁关联的条件变量。在使用这个条件变量之前,当前线程必须占用锁。调用Condition的await方法,会在等待之前原子地释放锁,并在等待被唤醒后原子的获取锁
1. 현재 스레드가 잠금을 획득합니다. 2. 현재 스레드를 중단합니다
tryLock If 호출 시 잠금을 획득할 수 있으며 잠금을 획득하고 true를 반환합니다. 현재 잠금을 획득할 수 없으면 이 메서드는 즉시 false를 반환합니다.
tryLcok(long time,TimeUnit 단위) 지정된 시간 내에 잠금을 획득하도록 시도합니다. 잠금을 획득할 수 있으면 잠금을 획득합니다. true를 반환하고 현재 잠금을 획득할 수 없으면 다음 세 가지 중 하나가 발생할 때까지 현재 스레드를 예약할 수 없습니다. 1. 현재 스레드가 잠금을 획득합니다. 2. 다른 스레드 중단으로 차단됨3. 지정된 대기 시간이 초과되었습니다
잠금 해제 현재 스레드가 차지하고 있는 잠금을 해제
newCondition 현재 잠금과 관련된 조건 변수를 반환합니다. 이 조건 변수를 사용하기 전에 현재 스레드가 잠금을 점유해야 합니다. 호출 조건의 wait 메서드는 기다리기 전에 원자적으로 잠금을 해제하고 깨어나기를 기다린 후에 원자적으로 잠금을 획득합니다.

다음으로 두 가지 잠금 및 잠금 해제 방법을 중심으로 전체 ReentrantLock이 어떻게 작동하는지 소개하겠습니다. ReentrantLock을 소개하기 전에 먼저 ReentrantLock의 클래스 계층 구조와 밀접하게 관련된 AbstractQueuedSynchronizer

1.2, ReentrantLock 클래스 계층

ReentrantLock을 구현합니다. Lock 인터페이스이며 Sync, NonfairSync 및 FairSync의 세 가지 내부 클래스가 있습니다. Sync는 AbstractQueuedSynchronizer를 상속하는 추상 유형입니다. 이 AbstractQueuedSynchronizer는 많은 잠금 관련 기능을 구현하고 tryAcquire, tryRelease와 같은 사용자 구현을 위한 후크 메서드를 제공하는 템플릿 클래스입니다. , 등. Sync는 AbstractQueuedSynchronizer의 tryRelease 메서드를 구현합니다. NonfairSync 및 FairSync 클래스는 Sync에서 상속하고 잠금 메서드를 구현한 다음 각각 공정한 선점과 불공정한 선점을 위해 서로 다른 tryAcquire 구현을 갖습니다.

1.3, AbstractQueuedSynchronizer

우선 AbstractOwnableSynchronizer를 상속받은 AbstractOwnableSynchronizer의 구현은 매우 간단하며, ExclusiveOwnerThread 변수는 내부적으로 사용됩니다. .

둘째, AbstractQueuedSynchronizer는 내부적으로 CLH 잠금 대기열을 사용하여 동시 실행을 직렬 실행으로 전환합니다. 전체 큐는 이중 연결 리스트입니다. CLH 잠금 대기열의 각 노드는 이전 노드와 다음 노드에 대한 참조, 현재 노드에 해당하는 스레드 및 상태를 저장합니다. 이 상태는 스레드가 차단되어야 하는지 여부를 나타내는 데 사용됩니다. 해당 노드의 이전 노드가 해제되면 현재 노드가 깨어나 선두가 됩니다. 새로 추가된 노드는 대기열 끝에 배치됩니다.

2. 부당한 잠금 방식

2.1, 잠금 방식 흐름도

2.2, 잠금 방식 상세 설명

1. ReentrantLock을 초기화할 때 매개변수가 공정한지 여부를 전달하지 않으면 기본적으로 NonfairSync인 불공정 잠금이 사용됩니다.

2. ReentrantLock의 잠금 메서드를 호출할 때 실제로는 NonfairSync의 잠금 메서드를 호출합니다. 이 메서드는 먼저 CAS 작업을 사용하여 잠금을 시도합니다. 성공하면 현재 스레드가 이 잠금에 설정되어 선점이 성공했음을 나타냅니다. 실패하면 획득 템플릿 메서드를 호출하고 선점을 기다립니다. 코드는 다음과 같습니다.

static final class NonfairSync extends Sync {
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
}

3. acquire(1) 호출은 실제로 잠금 선점 템플릿 집합인 AbstractQueuedSynchronizer의 획득 메서드를 사용합니다. 이는 If에 먼저 잠금을 획득하려고 시도하는 것입니다. 획득이 성공하지 못한 경우 CLH 대기열에 현재 스레드의 노드를 추가하면 선점을 기다리고 있음을 나타냅니다. 그런 다음 CLH 대기열의 선점 모드에 들어가면 잠금을 획득하는 작업도 수행합니다. 잠금을 획득할 수 없으면 LockSupport.park가 호출되어 현재 스레드를 일시 중지합니다. 그렇다면 현재 스레드는 언제 깨어날까요? 잠금을 보유하고 있는 스레드가 잠금 해제를 호출하면 CLH 대기열의 헤드 노드 옆에 있는 노드에서 스레드를 깨우고 LockSupport.unpark 메서드를 호출합니다. 획득 코드는 다음과 같이 비교적 간단합니다.

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

3.1 tryAcquire 후크 메서드는 잠금을 다시 획득하기 위해 먼저 획득 메서드 내부에서 사용됩니다. 이 메서드는 실제로 NonfairSync 클래스에서 nonfairTryAcquire를 사용합니다. 구현 원칙은 먼저 현재 잠금 상태가 0인지 비교합니다. 0이면 원자적으로 잠금을 시도합니다(상태를 1로 설정한 다음 현재 잠금 상태가 아닌 경우 현재 스레드를 배타적 스레드로 설정). 0, 현재 스레드를 비교하고 잠금을 점유하는 스레드가 스레드입니까? 그렇다면 상태 변수의 값이 증가합니다. 재진입 잠금이 가능한 이유는 동일한 스레드가 가능하다는 것을 알 수 있습니다. 그것이 차지하는 잠금을 반복적으로 사용하십시오. 위의 조건 중 어느 것도 통과하지 않으면 실패 시 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;
        }

3.2 tryAcquire가 false를 반환하면 CLH 대기열 기반 선점 모드인 acquireQueued 프로세스로 들어갑니다.

3.2.1. CLH 잠금 대기열 끝에 대기 노드를 추가합니다. 이 노드는 addWaiter를 호출하여 구현됩니다. 여기서는 첫 번째 대기 노드가 진입할 때 헤드 노드를 초기화해야 합니다. 그런 다음 현재 노드를 꼬리에 추가합니다. 후속 조치는 바로 끝에 노드를 추가하는 것입니다.

코드는 다음과 같습니다.

private Node addWaiter(Node mode) {
		// 初始化一个节点,这个节点保存当前线程
        Node node = new Node(Thread.currentThread(), mode);
        // 当CLH队列不为空的视乎,直接在队列尾部插入一个节点
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
		// 当CLH队列为空的时候,调用enq方法初始化队列
        enq(node);
        return node;
}

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // 初始化节点,头尾都指向一个空节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {// 考虑并发初始化
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
}

3.2.2 CLH 대기열에 노드를 추가한 후 acquireQueued 메서드를 입력합니다.

우선 외부 레이어는 무한 for 루프입니다. 현재 노드가 헤드 노드의 다음 노드이고 tryAcquire를 통해 잠금을 얻은 경우 헤드가 노드가 잠금을 해제하고 현재 스레드가 헤드 노드의 스레드에 의해 깨어납니다. 이때 현재 노드를 헤드 노드로 설정하고 failed 플래그를 false로 설정한 후 반환할 수 있습니다. 이전 노드의 경우 다음 변수는 null로 설정되고 다음 GC 중에 지워집니다.

如果本次循环没有获取到锁,就进入线程挂起阶段,也就是shouldParkAfterFailedAcquire这个方法。

代码如下:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}

3.2.3、如果尝试获取锁失败,就会进入shouldParkAfterFailedAcquire方法,会判断当前线程是否挂起,如果前一个节点已经是SIGNAL状态,则当前线程需要挂起。如果前一个节点是取消状态,则需要将取消节点从队列移除。如果前一个节点状态是其他状态,则尝试设置成SIGNAL状态,并返回不需要挂起,从而进行第二次抢占。完成上面的事后进入挂起阶段。

代码如下:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            //
            return true;
        if (ws > 0) {
            //
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

3.2.4、当进入挂起阶段,会进入parkAndCheckInterrupt方法,则会调用LockSupport.park(this)将当前线程挂起。代码:

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
}

三、 非公平锁的unlock方法

3.1、unlock方法的活动图

3.2、unlock方法详细描述

1、调用unlock方法,其实是直接调用AbstractQueuedSynchronizer的release操作。

2、进入release方法,内部先尝试tryRelease操作,主要是去除锁的独占线程,然后将状态减一,这里减一主要是考虑到可重入锁可能自身会多次占用锁,只有当状态变成0,才表示完全释放了锁。

3、一旦tryRelease成功,则将CHL队列的头节点的状态设置为0,然后唤醒下一个非取消的节点线程。

4、一旦下一个节点的线程被唤醒,被唤醒的线程就会进入acquireQueued代码流程中,去获取锁。

具体代码如下:

unlock代码:

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

release方法代码:

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
}

Sync中通用的tryRelease方法代码:

protected final boolean tryRelease(int releases) {
     int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
          free = true;
          setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
 }

unparkSuccessor代码:

private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null) 
            LockSupport.unpark(s.thread);
}

四、 公平锁和非公平锁的区别

公平锁和非公平锁,在CHL队列抢占模式上都是一致的,也就是在进入acquireQueued这个方法之后都一样,它们的区别在初次抢占上有区别,也就是tryAcquire上的区别,下面是两者内部调用关系的简图:

NonfairSync
lock —> compareAndSetState
                | —> setExclusiveOwnerThread
      —> accquire
		     | —> tryAcquire
                           |—>nonfairTryAcquire
                |—> acquireQueued

FairSync
lock —> acquire
               | —> tryAcquire
                           |—>!hasQueuePredecessors
                           |—>compareAndSetState
                           |—>setExclusiveOwnerThread
               |—> acquireQueued

真正的区别就是公平锁多了hasQueuePredecessors这个方法,这个方法用于判断CHL队列中是否有节点,对于公平锁,如果CHL队列有节点,则新进入竞争的线程一定要在CHL上排队,而非公平锁则是无视CHL队列中的节点,直接进行竞争抢占,这就有可能导致CHL队列上的节点永远获取不到锁,这就是非公平锁之所以不公平的原因。

五、 总结

线程使用ReentrantLock获取锁分为两个阶段,第一个阶段是初次竞争,第二个阶段是基于CHL队列的竞争。在初次竞争的时候是否考虑队列节点直接区分出了公平锁和非公平锁。在基于CHL队列的锁竞争中,依靠CAS操作保证原子操作,依靠LockSupport来做线程的挂起和唤醒,使用队列来保证并发执行变成了串行执行,从而消除了并发所带来的问题。总体来说,ReentrantLock是一个比较轻量级的锁,而且使用面向对象的思想去实现了锁的功能,比原来的synchronized关键字更加好理解。

위 내용은 Java의 재진입 잠금 원리를 설명하는 자세한 샘플 코드의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.