Java のリエントラント ロックの原理を説明する詳細なサンプル コード

黄舟
リリース: 2017-03-22 11:07:29
オリジナル
1833 人が閲覧しました

1. 概要

この記事では、まず Lock インターフェース、ReentrantLock のクラス階層、およびロック関数テンプレート クラス AbstractQueuedSynchronizer の簡単な原理を紹介し、次に ReentrantLock のロック メソッドとロック解除メソッドを分析することで、ReentrantLock の内部原理を説明します。 、最後にまとめをします。この記事では、ReentrantLock の条件変数については説明しません。

1.1. ロックインターフェース

ロックインターフェースは同時実行性を制御するためのツールを抽象化したものです。 synchronized キーワードを使用するよりも柔軟性があり、条件変数をサポートできます。一般的には、特定の共有リソースの排他性を制御するためのツールです。つまり、このロックを取得してリソースを同時に占有できるのは 1 つのスレッドだけです。他のスレッドがロックを取得したい場合は、このスレッドがロックを解放するまで待つ必要があります。 Java 実装における ReentrantLock はそのようなロックです。もう 1 つの種類のロックは、複数のスレッドがリソースを読み取ることを許可しますが、1 つのスレッドのみがリソースを書き込むことができる、読み取り/書き込みロックと呼ばれる特殊なロックです。以下は、Lock インターフェースのいくつかのメソッドの全体的な説明です。

ロックを解除しますnewCondition

次に、ロックとロック解除の 2 つの方法を中心に ReentrantLock 全体がどのように動作するかを紹介します。 ReentrantLock を紹介する前に、まず ReentrantLock とその密接に関連する AbstractQueuedSynchronizer

1.2、ReentrantLock クラス階層を見てみましょう

ReentrantLock は Lock インターフェイスを実装しており、3 つの内部クラス Sync と NonfairSync 、FairSync を持っています。 , Sync は、AbstractQueuedSynchronizer を継承する抽象型です。この AbstractQueuedSynchronizer は、多くのロック関連の関数を実装し、ユーザーが実装するためのフック メソッド (tryAcquire、tryRelease など) を提供するテンプレート クラスです。 Sync は、AbstractQueuedSynchronizer の tryRelease メソッドを実装します。 NonfairSync クラスと FairSync クラスは Sync を継承し、lock メソッドを実装してから、それぞれ公平なプリエンプションと不公平なプリエンプションのための tryAcquire の異なる実装を持ちます。

1.3、AbstractQueuedSynchronizer

まず、AbstractQueuedSynchronizer は AbstractOwnableSynchronizer を継承しており、排他的シンクロナイザーを表し、変数 exclusiveOwnerThread が排他的スレッドを表すために内部的に使用されます。

2 番目に、AbstractQueuedSynchronizer は内部で CLH ロック キューを使用して、同時実行をシリアル実行に変換します。キュー全体は二重リンクリストです。 CLH ロック キュー内の各ノードは、前のノードと次のノード、現在のノードに対応するスレッド、および状態への参照を保存します。このステータスは、スレッドをブロックする必要があるかどうかを示すために使用されます。ノードの前のノードが解放されると、現在のノードが目覚めて先頭になります。新しく追加されたノードはキューの最後に配置されます。

2. 不公平なロックのロック方法

2.1. ロック方法のフローチャート

2. ロック方法の詳細な説明

1. ReentrantLock を初期化するときに、それらがロックされているかどうかを確認します。フェアの場合、デフォルトはアンフェア ロック (NonfairSync とも呼ばれます) が使用されます。

2. ReentrantLock の lock メソッドを呼び出すと、このメソッドは最初に 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 の取得メソッドが使用されます。成功すると、現在のスレッドのノードを追加して、プリエンプションを待っていることを示します。その後、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. まず、待機ノードを追加します。このノードは、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 を通じてロックが取得された場合、それはヘッド ノードがロックを解放し、現在のノードを取得したことを意味します。スレッドがヘッド ノードのスレッドによって起動されると、現在のノードをヘッド ノードとして設定し、失敗フラグを 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关键字更加好理解。

メソッド名 説明
lock ロックを取得できない場合、現在のスレッドは、ロックが取得されます
lockInterruptibly 現在のスレッドが中断されない限り、ロックは取得されます。ロックが取得された場合は、すぐに戻ります。取得できない場合、現在のスレッドはスケジュール不能になり、次の 2 つのことが起こるまでスリープします:

1. 現在のスレッドがロックを取得する

2. 他のスレッドが現在のスレッドを中断する

tryLock 呼び出し時にロックを取得できた場合は、ロックを取得してtrueを返します。現在のロックが取得できなかった場合、このメソッドはすぐにfalseを返します
tryLcok(long time, TimeUnit)。ユニット) 指定された時間内にロックを取得しようとします。ロックを取得できる場合は、ロックを取得し、true を返します。現在のロックを取得できない場合は、次の 3 つのいずれかが発生するまで、現在のスレッドはスケジュール不能になります。 :

1. 現在のスレッドがロックを取得しました

2. 現在のスレッドが他のスレッドによって中断されました

現在のロックに関連付けられた条件変数を返します。この条件変数を使用する前に、現在のスレッドがロックを占有している必要があります。 Condition の await メソッドを呼び出すと、待機する前にアトミックにロックが解放され、目覚めるのを待った後にアトミックにロックが取得されます

以上がJava のリエントラント ロックの原理を説明する詳細なサンプル コードの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート