Javaの並行処理 - 基本的な知識のスレッドロック - 原則の適用と深さのJava AQS同期

並行プログラミング、私たちはしばしば言葉AQSを参照してください、私の友人の多くは、ブロガーが直接テーマに、最終的には理解していくつかの情報を読んでどのようなもの、知りません。

ブリーフ

AQSは略語AbstractQueuedSynchronizerクラスで、これは言うまでもない、私たちはEclipseでこのクラスを入力するには、クラスがjava.util.concurrent.locksパッケージに抽象クラスである知っていますよ。ReentrantLockの、セマフォ、たCountDownLatch、ReentrantReadWritLock、ので、なぜ私たちは、この抽象クラスの分析に焦点を当てる必要があるん ThreadPoolExecutorは、 すべてのAQSに基づいて実装されている、AQSは、基本クラスJUC(java.util.concurrentの)ツールキットです。すべては、AQSは、並行プログラミングがクラスを認識する必要があります学ぶことは非常に重要であると述べました。

ビューソースReentrantLockのは、あなたがAQSは継承によって、それに対応する機能を実現することであり、内部クラスで学ぶことができるので、内部の抽象クラスを継承同期AbstractQueuedSynchronizerクラスがあることを、他のクラスは、同様の使用に関連している見ることができます。

public class ReentrantLock implements Lock, java.io.Serializable {
   abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;
        .......
    }

    # 非公平锁
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        .....
    }
    
    #公平锁
    static final class FairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        .....
    }
}

次のようにAQS公式コメントは(あなたがを直接見、見にあまり感じる場合は、このセクションは無視することができます説明が与えられた要約部分):

最初アウト(FIFO)キューと関連シンクロナイザブロッキングロックス(セマフォ、イベント、等)第提供するフレームワークに基づきます。このクラスは、シンクロナイザは、個々の原子{@codeのINT}値が示す状態に依存して、最も有用なタイプの同期ベースであるように設計されています。サブクラスは、これらのメソッドの保護状況を変更する方法を定義しなければならないオブジェクトの状態のために、この手段は、取得または解放されるものを定義します。これらの図では、このクラスは、キューイングおよびメカニズムを遮断する他のすべてのメソッドを実行します。サブクラスは、他の状態フィールドを維持することができるが、唯一の利用{@link #getState}、および{@link #compareAndSetState}自動的に更新された値{リンク#setState @} {@code intは}同期追跡されます。

サブクラスは、クラスを囲む同期特性のために使用される非共通内部クラスを定義助けるべきです。{@code AbstractQueuedSynchronizer}クラスは、同期インタフェースを実装していません。その代わりに、それは{@link #acquireInterruptibly}ロック特に好適に呼び出すことができる他の方法およびそれらの一般的な方法を達成するために、関連するシンクロナイザを定義します。

このような支援1またはデフォルトの排他モードと共有モードの両方。排他モードで取得する場合、取得しようとし、他のスレッドは成功しません。(必須ではないが)成功する可能性モデルを共有する複数のスレッドによって獲得されます。このクラスは、成功のための共有モデル、待つ次のスレッド(もしあれば)もそれを得ることができるかどうかを決定しなければならないとき、機械的な意味に加えて、これらの違いを理解していません。異なるモードで待機しているスレッドは、同じFIFOキューを共有しています。典型的には、実装サブクラスのみが一つのモードをサポートする2つのモードは、{@link ReadWriteLock}において、例えば、役割を有します。この方法が使用されていないサポートするだけ排他モードまたは共有モードのみをサポートすることは、サブクラスの定義モードをサポートする必要はありません。

このクラスは、ネストされたクラス{@linkたConditionObject}を定義し、{}サブクラス@リンクを達成するための方法をサポートする排他モードとして使用することができる{@link#isHeldExclusively}だけ現在のスレッドの同期を報告{保持します@呼び出しリリースリンク#}とオブジェクトの{@link#getStateを}電流値が完全に解除され、{@リンク#}の方法は、最終的に先に得られたこのオブジェクトの状態に戻り、保存された状態値を考慮して、得られます。どれ{@code AbstractQueuedSynchronizer}この方法は、あなたがこの制約を満たすことができない場合ので、それを使用していない、そのような状態を作成しません。もちろん、{リンクConditionObjectの@}その挙動が達成され、同期のセマンティクスに依存します。

このクラスは、方法およびプロセス条件類似のオブジェクトをテストおよび監視、キュー内部検査を提供します。同期を達成するためにそれらに露出所望{@code AbstractQueuedSynchronizer}クラスとして使用することができます。

そう、デシリアライズのみメンテナンス状態の基礎となる整数の原子店のこのクラスのシーケンスは、可能なスレッドキューオブジェクト。サブクラスの典型的なオブジェクトのデシリアライゼーションは、既知の初期状態に復帰シーケンス{@codeのreadObject}メソッドを定義する必要があります。

AQSの概要

原理の実現: AQSサブクラスは、同期特性閉じクラスのために使用され、それに対応する同期メソッド呼び出しによって達成される非公開内部ヘルパークラスとして定義されなければなりません。

これは、コンテンツの深い理解が必要です。

先入れ先出し(FIFO)キューは、どのような原理を実現するために待っていますか? - FIFO(FIFO)キューブロッキング・ロックと関連シンクロナイザに基づいてAQS

AQSから国家の役割は何ですか? - このクラスは、シンクロナイザは、個々の原子{@code INT}値が示す状態に依存して、最も有用なタイプの同期ベースであるように設計されています

ロックを達成し、ロックを解除する方法を、排他モードロックをAQSとどのように共有モードロックを達成するために?

CLHキュー実装の原則

AQSの核となるアイデアは、要求された共有リソースがフリーであれば、その後の仕事に現在のリクエストスレッドのリソースを効率的にスレッド、およびロックされた状態に共有リソースということです。要求された共有リソースが占有されている場合は、あなたが起こされるように割り当てられ、ロック機構用ブロックするスレッドと待機を必要とするあまり、一時的により取得スレッドがキューに追加されたロックしようとしている、CLHロックキューによって実装されているこのメカニズムをAQS。

CLH(クレイグ、とLandin、及びヘゲルステン)仮想キューは、双方向キュー(本例ではない両端キュー仮想キューは、ノード間の関係のみが存在する)です。各要求スレッドがロックを達成するために割り当てられた共有リソースロックキューCLHノード(ノード)としてパッケージ化されAQS。

 次のようにCLHキュー構造は次のとおりです。

赤ノードが最初のノードであるノードがロックを保持しているように、それは使用することができ、ソースノードの内部クラスは、キューのヘッド(ヘッド)とクラスキューの実装CLHとAQSテール(尾部)であります

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    static final class Node {...}
    private transient volatile Node head;
    private transient volatile Node tail;
    private volatile int state;//同步状态

 

以上のことから、それは頭と尾揮発性のために設定され、これら2つのノードが他のスレッドによって見られる、変更、実際には、我々は大きく2つのノードを変更するために、チームとチームによって行われてきました。

static final class Node {
    //该等待同步的节点处于共享模式
    static final Node SHARED = new Node();
    //该等待同步的节点处于独占模式
    static final Node EXCLUSIVE = null;

    //等待状态,这个和state是不一样的:有1,0,-1,-2,-3五个值
    volatile int waitStatus;
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;

    volatile Node prev;//前驱节点
    volatile Node next;//后继节点
    volatile Thread thread;//等待锁的线程
    //和节点是否共享有关
    Node nextWaiter;
    //Returns true if node is waiting in shared mode
    final boolean isShared() {
            return nextWaiter == SHARED;
    }

次の5つの下waitStatusの意味を説明します。

  • CANCELED(1):この状態では、ノードステータスがキャンセル(ボイド)のままになり、したがって、キューから削除されるべきであるならば、タイムアウトにより又は中断では、キャンセルされた(ボイド)の状態にすることができるノードをスレッド。
  • SIGNAL(-1):この信号ノードであり、後続ノードが中断され、そして覚醒しなければならない(unparking)現在のノードにおけるその後続ノード解除後またはロックを解除します。
  • CONDITION(-2)待機状態条件におけるスレッドのノード、それが起こされるまで同期キュー内のノードであるとみなされていない(信号)を、0組の値は、ブロックされた状態に再び入ります。
  • 0:新しいノード

ロックが取得されたときに、必ずしも一つだけスレッドとしたがって、この場合には、(別段の同期状態とも呼ばれる)のロックを保持することができる排他モードと共有モード、nextWaitによって識別されるノードのノードである差。例えばReentrantLockの排他ロック、ロックを取得する唯一のスレッドであるが、ロックがWriteAndReadLockを読み取ることができる複数のスレッドによって同時に取得されたが、それは唯一のスレッドが保持しているロックを書き込むことができます。。最初の取得を紹介しに(そうでなければ同期状態とも呼ばれる)排他モードでは、このクラスの用途をロックを解除テンプレートモデル設計(特にボーエン関連する後続の設計パターンを表示することが理解される)アルゴリズムの動作のスケルトン定義、および遅延は、サブクラスへのステップのいくつかを達成します。

独占取得ロック

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

現在のキューノードの端部に第一(tryAcquire(引数)が定義サブクラスで具現化される)ロックを取得しようとする試み、取得された場合、終了する、またはaddWaiter(Node.EXCLUSIVE)によって、引数)方法そして、排他的に設定

private Node addWaiter(Node mode) {
        //把当前线程包装为node,设为独占模式
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        //如果tail不为空,把node插入末尾
        if (pred != null) {
            node.prev = pred;
            //此时可能有其他线程插入,所以重新判断tail
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
}

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //此时可能有其他线程插入,所以重新判断tail是否为空
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
}

テール・ノードが空である場合、実行ENQ(ノード);リトライキューノードの端部が挿入された後、最終のノードが挿入され、それはすぐにスレッドの前に、その挿入中に、ノード内のスレッドを中断しません。それはスピニング操作acquireQueued(ノード、引数を)させて頂きますので、実行されている可能性があり、再獲得ロックにスレッドを取得しよう!条件が満たされたときに取得するために、スピンプロセスからの出口をロックし、それ以外の場合は継続。

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);
        }
}

あなたがロックを取得しない場合は、一時停止すべきか否かが判断され、これは裁判官waitStatusその先行ノードによって決定されなければなりません。

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;
}

waitStatusの先行ノードがある場合:

  • SIGNALは、現在のスレッドがスレッドの保留、中断しなければならない場合は、trueを返す、と私たちは現在のスレッドが中断された発見した場合の検出は例外:InterruptedExceptionをスローし、ループを終了し、目を覚ます割り込み後、起こされるのを待っています。
  • > 0、前駆蹴らノードキュー、リターンの偽
  • 場合<0、また次の決意は、現在のノードがハングするように、falseを返すが、信号waitStatusノードへの最初の前駆体。

最後に、我々は排他ロックを取得する処理の概要を行います。

AQSのスピンに>ノード- >ノードスレッド構成されたノード(addWaiter) - - >ノードは、同期キューのテール(addWaiter)にノードを追加するカスタム実装tryAcquire状態同期失敗を取得サブクラス取得のメソッドを呼び出すことにより、テンプレート同期状態を取得する方法(acquirQueued)。ノードの同期は、スピン状態を取得する際の前駆体は、このノードの前駆体はヘッドノードでない場合、または、ヘッドノードは、同期失敗状態を取得する単一のノードは、それが決定されることである場合、その前駆体ノードである場合にのみ、ヘッドノードは、取得同期状態にしようとしそれは後に起こされるためにリターンニーズを阻止する必要がある場合、現在のスレッドは、ブロックする必要があります。

排他モード同期状態をリリース

それが解放されるので、スレッドはヘッドノードでロックを解除し、ロック解除操作を保持しているスレッドが実行される確かです。

、解放状態と解放同期取得同期取得状態をAQSテンプレート方法は、特定の操作tryReleaseリリースは親AQSアルゴリズムが唯一のバックボーンを提供し、達成するためにサブクラスがあります。

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

/**如果node的后继节点不为空且不是作废状态,则唤醒这个后继节点,否则从末尾开始寻找合适的节点,如果找到,则唤醒*/
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);
        }
}

 プロセス:最初のコールtryReleaseサブクラス()ロックを解除し、その後のきっかけに後続ノードをウェイクアップするための方法は、必要性は、そうでなければ、後続ノードではないと無効状態、後続ノードをウェイクアップでない場合、後続ノードは、状況を満たすためにかどうかを決定しますテール・ノードの適切なノードから楽しみにして、見つかった場合は、その後、目覚めます。

共有ロック

ロック取得処理:

  1. スレッドがロックリソースを取得するacquireShared()アプリケーションを呼び出すと、成功した場合、クリティカル領域を入力します。
  2. ロックが失敗した取得する場合、ノードのとFIFOキューに共有タイプを作成し、その後、ウェイクを待つために停止しました。
  3. スレッドキューを再び起こされるのを待っているときに成功した場合、リソースのロックを取得しようと共有ノードの後ろの航跡とはまだ一緒に渡すためにウェイクアップイベントを待っているノードの背面にあるすべての共有ノード目覚め、順番にその意志を、その後、入力クリティカルセクションは、そうでない場合は待機をハングアップし続けています。

ロック解除方法:

  1. スレッドがリリースされるreleaseShared()のロックリソースを呼び出すとリリースが成功した場合、もしあれば、キュー内のノードの待ち時間は、目覚めています。

ソースコードの共有ロックの詳細な分析

:上記共有ロックが実行されるプロセスに基づいて、我々は、論理を実装するソースコードを見て
次のように、acquireShared方法における最初の外観は、(ロックを取得します)

 

   public final void acquireShared(int arg) {
        //尝试获取共享锁,返回值小于0表示获取失败
        if (tryAcquireShared(arg) < 0)
            //执行获取锁失败以后的方法
            doAcquireShared(arg);
    }

ここでtryAcquireShared()メソッドは、特定の論理取得ロックを達成するために、ユーザに任されています。メソッドの実装については、特に二つのことがあります。

まず、この方法はさらに、買収によってサポートされている場合、現在のコンテキストの取得に共有ロックをサポートするかどうかを自分で確認する必要があります。

第二に、メソッドの戻り値はキーです。まず、上記のコードフラグメントは、戻り値がより少ない0がキューを入力する必要があり、ロックの獲得に失敗したことを示している以上であることがわかります。戻り値が0に等しい場合は第二に、現在のスレッドが共有ロックの成功を獲得することを示しているが、それは、その背後に待機しているウェイクノードにする必要はありませんされて得られた糸を、従うことを継続することはできません。最後に、戻り値が0より大きい場合は、可能性の高い共有ロックを獲得し続けることもあり、現在のスレッドがノードを待っている共有ロックとその後の成功を獲得することを示している共有ロックを取得するノードの試みを覚ますためにそれらを追跡するために、この時間が必要で言うことですつまり、成功しています。

上記の契約により、当社はdoAcquireSharedを達成するための方法を見ていきます。

 

    //参数不多说,就是传给acquireShared()的参数
    private void doAcquireShared(int arg) {
        //添加等待节点的方法跟独占锁一样,唯一区别就是节点类型变为了共享型,不再赘述
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //表示前面的节点已经获取到锁,自己会尝试获取锁
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    //注意上面说的, 等于0表示不用唤醒后继节点,大于0需要
                    if (r >= 0) {
                        //这里是重点,获取到锁以后的唤醒操作,后面详细说
                        setHeadAndPropagate(node, r);
                        p.next = null;
                        //如果是因为中断醒来则设置中断标记位
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //挂起逻辑跟独占锁一样,不再赘述
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //获取失败的取消逻辑跟独占锁一样,不再赘述
            if (failed)
                cancelAcquire(node);
        }
    }

最初のノードを設定して、成功した、プロセスが終了した後、排他ロック状態を取得し、割り込みモードに戻ります。成功した共有ロックモードを取得した後、メソッドの名前から呼び出しsetHeadAndPropagate方法は、次のコードを参照、新しいヘッドノードが推移作用を有する設定に加えて、見ることができます。

 

    //两个入参,一个是当前成功获取共享锁的节点,一个就是tryAcquireShared方法的返回值,注意上面说的,它可能大于0也可能等于0
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; //记录当前头节点
        //设置新的头节点,即把当前获取到锁的节点设置为头节点
        //注:这里是获取到锁之后的操作,不需要并发控制
        setHead(node);
        //这里意思有两种情况是需要执行唤醒操作
        //1.propagate > 0 表示调用方指明了后继节点需要被唤醒
        //2.头节点后面的节点需要被唤醒(waitStatus<0),不论是老的头结点还是新的头结点
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //如果当前节点的后继节点是共享类型或者没有后继节点,则进行唤醒
            //这里可以理解为除非明确指明不需要唤醒(后继等待节点是独占类型),否则都要唤醒
            if (s == null || s.isShared())
                //后面详细说
                doReleaseShared();
        }
    }

    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

:最後のウェイクアップ動作も非常に複雑で、分析するためにアウト専門
注:この操作は、ウェイクreleaseShare()メソッドで呼び出されます。

 

private void doReleaseShared() {
        for (;;) {
            //唤醒操作由头结点开始,注意这里的头节点已经是上面新设置的头结点了
            //其实就是唤醒上面新获取到共享锁的节点的后继节点
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //表示后继节点需要被唤醒
                if (ws == Node.SIGNAL) {
                    //这里需要控制并发,因为入口有setHeadAndPropagate跟release两个,避免两次unpark
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;      
                    //执行唤醒操作      
                    unparkSuccessor(h);
                }
                //如果后继节点暂时不需要唤醒,则把当前节点状态设置为PROPAGATE确保以后可以传递下去
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            //如果头结点没有发生变化,表示设置完成,退出循环
            //如果头结点发生变化,比如说其他线程获取到了锁,为了使自己的唤醒动作可以传递,必须进行重试
            if (h == head)                   
                break;
        }
    }

そして、ロック解除を共有するプロセスを見てみましょう。

 

public final boolean releaseShared(int arg) {
        //尝试释放共享锁
        if (tryReleaseShared(arg)) {
            //唤醒过程,详情见上面分析
            doReleaseShared();
            return true;
        }
        return false;
    }

注:上記setHeadAndPropagate()メソッドは、待機しているキューのスレッドが正常に共有ロックを取得し、それは(もしあれば)その背後にあるノードを共有して目を覚ます必要があり、この時間を表しますが、共有ロックの時間を解放することによってreleaseShared()メソッドそして、取得しようとする共有ロックのスレッドと排他ロックを待つ覚醒させることができます。

第三に、要約

排他ロックと比較すると、主な機能は、待ち行列を共有するノードの後に​​共有ロックが正常にロックを獲得したときに、それが共有されているので、それはすべてのウェイク背後に従わなければならない、(それは共有ロックを取得します)ということです現在のノードロックリソースとそれを共有し、これらのノードはまた、これは排他ロック待ちは、それがすでにロックを獲得するために共有ノードを持っている、とあれば、それは確かに取得していないことが前提である(共有ロックを待機しなければならないことは間違いありません)へ。共有ロックが解放されると、その後、読み取りロックまたは書き込みロックのいずれかが、すべてのリソースの競合することができ、読み取りロックが解除されたときに、読み書きロックに考えることを例することができます。

マイクロチャネルパブリック番号、良い日の記事の著者:

 

公開された116元の記事 ウォン称賛83 ビュー280 000 +

おすすめ

転載: blog.csdn.net/belvine/article/details/104798795