ReentrantLock ソース コードにおける AQS 原理の詳細な分析

AQS の概要

AQS の完全な名前はAbstractQueuedSynchronizerであり、FIFO 双方向リンク リストを内部的に実装する抽象クラスです。リンク リストの各ノードには前のノードへのポインタと次のノードへのポインタがあるため、AQS はすぐにアクセスできます任意のノードからの先行ノードと次のノード。その後、各ノードはスレッドにバインドされます。スレッドがロックの競合に失敗すると、スレッドはキューの最後尾に追加され、解放されるまで待機します。ロックが解除されると、スレッドはキューの末尾に追加され、解放されるまで待機します。解放されると、キューのヘッド ノードのスレッドが解放され、ロックを競合します。

ReentrantLock は、Lock インターフェイスの実装クラスです。これは一般的に使用されるオブジェクト同期ロックであり、リエントラント ロックです。リエントラント ロックとは、スレッドがロックを取得した後、ロックを再度取得するためにブロックする必要がないことを意味しますですが、カウンターに直接関連付けられています。再エントリの数を増やします。詳細については、この記事を参照してください。

ReentrantLock は内部クラス Sync をカプセル化し、AbstractQueuedSynchronizer 抽象クラスを継承します。ReentrantLock ロックの原理は Sync に基づいています。ソース コードを見てみましょう。

// 加锁
public void lock() {
    
    
	sync.lock();
}
// 解锁
public void unlock() {
    
    
	sync.release(1);
}

この記事では、ReentrantLock のソースコードから AQS の実装原理を分析します。

ReentrantLock のソース コード

ReentrantLock は、Sync の 2 つのサブクラス、つまり NonfairSync と FairSync の静的内部クラスもカプセル化します。NonfairSync は文字通り、不公平なロックであることを意味し、FairSync は公正なロックです。この記事では主に NonfairSync について分析します

  • NonfairSync.lock
final void lock() {
    
    
	//通过cas操作来修改state状态,表示争抢锁的操作
    if (compareAndSetState(0, 1))	
    	// 设置当前获取到锁的线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);		// 未获取到锁的线程再次尝试获取锁
}

このコードは簡単に説明します

  • ロックが正常に取得された場合は、最初に CAS に移動してロックを取得します。
  • ロックを正常に取得した現在のスレッドを保存します。
  • ロックの取得に失敗しました。取得を呼び出してロック競合ロジックを実行します。

もう一度compareAndSetStateを見てください

protected final boolean compareAndSetState(int expect, int update) {
    
    
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

CompareAndSetState は、CAS メソッドを使用して状態を 1 に設定します。 CAS メソッドは、変更される内容が期待値と同じかどうかを比較します (期待値は変更前の値であり、変更前に他のスレッドによって変更される可能性があります)。なので、まず期待値と一致するかどうかを判断します)期待値が同じです)、同じ修正が完了した場合はtrueを返し、同じでない場合は修正は失敗してfalseを返します、この一連の動作はアトミックな操作です。CASの原理はここで参照できます

AbstractQueuedSynchronizer は、CAS に基づいて変数値を設定するいくつかのメソッドをカプセル化しています。ソース コードを見てみましょう。

	private static final Unsafe unsafe = Unsafe.getUnsafe();
	private static final long stateOffset;
	private static final long headOffset;
	private static final long tailOffset;
	private static final long waitStatusOffset;
	private static final long nextOffset;
	
	static {
    
    
	   try {
    
    
	       stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
	       headOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
	       tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
	       waitStatusOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("waitStatus"));
	       nextOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("next"));
	   } catch (Exception ex) {
    
     throw new Error(ex); }
	}
	
	private final boolean compareAndSetHead(Node update) {
    
    
	   return unsafe.compareAndSwapObject(this, headOffset, null, update);
	}
	
	private final boolean compareAndSetTail(Node expect, Node update) {
    
    
	   return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
	}
	
	private static final boolean compareAndSetWaitStatus(Node node, int expect,int update) {
    
    
	   return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update);
	}
	
	private static final boolean compareAndSetNext(Node node, Node expect, Node update) {
    
    
	   return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
	}

Unsafe の CAS メソッド

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

この部分の核心は Unsafe クラスのメソッドを呼び出すことです Unsafe クラスについては、前回の CAS 原理の解析記事で紹介しましたが、CPU ベースでメモリを操作するための (ネイティブ) ローカル メソッドを提供します命令セット。各メソッドには 4 つの入力パラメータがあります。2 番目の入力パラメータは、AbstractQueuedSynchronizer のいくつかのメンバー変数のメモリ内のアドレスです。これらのメソッドの一般的な実装プロセスは、最初に変数アドレスを通じて変数のメモリ値を取得し、次に値が同じであれば、update Value が true を返し、それ以外の場合は何もせずに false を返します。

ロック メソッドに戻ると、ロックを取得するプロセスは次のように理解できます。

  • state=0 の場合、ロック状態がないことを意味します
  • state>0 の場合、スレッドがロックを取得したことを意味します。CAS を通じてこれを 1 に変更したスレッドはロックを正常に取得します。ただし、ReentrantLock はリエントラントを許可するため、同じスレッドが複数回同期ロックを取得すると、状態がロックを取得します。たとえば、再エントリが 5 回行われると、state=5 になります。ロックを解放するときは、state=0 の他のスレッドがロックを取得できるようになるまで、ロックを 5 回解放する必要があります。

取得

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

このメソッドの主なロジックは次のとおりです。

  • tryAcquire を通じて排他ロックの取得を試行し、成功した場合は true を返し、失敗した場合は false を返します。
  • tryAcquire が失敗した場合、現在のスレッドはノードにカプセル化され、addWaiter メソッドを通じて AQS キューの最後に追加されます。
  • acquireQueuedは、Nodeをパラメータとして受け取り、スピンすることによってロックを取得しようとします。

Nodeは
スレッドを節約するためにAQSキュー内のNode内部クラスをカプセル化します.NodeはFIFO二重リンクリストのデータ構造です.この構造の特徴は、各データ構造がそれぞれノードの後続ノードを指す2つのポインタを持つことです.そしてプリカーサーノード。各ノードは実際にはスレッドによってカプセル化されており、スレッドがロックの競合に失敗すると、ノードとしてカプセル化され、ASQ キューに追加されます。

ノードのソースコード:

static final class Node {
    
    
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1; 
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;	// 等待状态标识
        volatile Node prev;		// 前节点
        volatile Node next;		// 后节点
        volatile Thread thread;	// 竞争锁的线程
        // 存储在condition队列中的后继节点
        Node nextWaiter;
		// 是否为共享锁
        final boolean isShared() {
    
    
            return nextWaiter == SHARED;
        }

        final Node predecessor() throws NullPointerException {
    
    
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {
    
        // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {
    
         // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) {
    
     // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

thread、prev、next はそれぞれ現在のスレッド、前のノード、次のノードであるため、ノード キューは任意のノードから開始して、前から後ろにキューの先頭と末尾まで移動できます。

NonfairSync.tryAcquire方法

protected final boolean tryAcquire(int acquires) {
    
    
	return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
    
    
	// 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取state值,即获取锁状态
    int c = getState();
    // state等于0表示无锁状态直接获取锁并返回
    if (c == 0) {
    
    
        if (compareAndSetState(0, acquires)) {
    
    
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果是已拿到锁的线程再次获取锁则state加上1,代表该锁被重入一次
    else if (current == getExclusiveOwnerThread()) {
    
    
        int nextc = c + acquires;  锁状态 + 1 表示重入次数
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 未获取到锁返回true
    return false;
}

tryAcquire メソッドは、排他ロックの取得を試行します。CAS は、最初のステップでロックの取得に失敗した後、ロックを AQS キューに追加する前に再度ロックを取得します。これは不公平なロックであるため、スレッドがロックを取得する可能性があります。 AQS キューに追加される前にロックを取得すると、この時点でロックが解放されます。このとき、背後のロックを競合するスレッドが、AQS キューを追加する前にロックをプリエンプトする可能性があるため、背後のロックを競合するスレッドが発生することがあります。ロックをより早くプリエンプトできます。

公平なロックでは、CAS でロックの取得に失敗した各スレッドは、先入れ先出しの原則に従って AQS キューに追加され、ロックを競合するスレッドがロックを奪取します。厳格な命令

AbstractQueuedSynchronizer.addWaiter方法

private Node addWaiter(Node mode) {
    
    
// 将线程封装到Node中,mode为EXCLUSIVE即为独占锁
    Node node = new Node(Thread.currentThread(), mode);
    // tail是AQS的一个属性,代表队列尾节点
    Node pred = tail;
    if (pred != null) {
    
    		// tail不为空的情况,说明队列已经被初始化即有节点数据
        node.prev = pred;	// 当前线程Node的前节点指向AQS队列尾节点
        if (compareAndSetTail(pred, node)) {
    
     	// 通过CAS方式将Node添加到AQS队列尾部
            pred.next = node;	// CAS成功则原来的AQS尾节点的后节点指向当前线程Node
            return node;
        }
    }
    enq(node);	// CAS失败或AQS队列没有节点数据则进入eq方法
    return node;
}

addWaiter メソッドのプロセス全体は、スレッドをノードにカプセル化し、それを CAS を通じて AQS キューの最後に追加することです。

AbstractQueuedSynchronizer.enq方法

private Node enq(final Node node) {
    
    
// 进入无限for循环,即自旋
    for (;;) {
    
    
        Node t = tail;	// 获取AQS队列尾节点
        if (t == null) {
    
     // tail为空表示AQS队列没有数据需要进行初始化
       		// 通过CAS方式初始化AQS队列即创建一个空Node作为队列头部
            if (compareAndSetHead(new Node()))	
                tail = head;  // CAS成功此时AQS队列只有一个节点,因此队列头尾都是该节点
        } else {
    
    	// 如果此时AQS队列已被初始化则将Node添加到队列尾部
            node.prev = t;	// Node节点指向AQS队列尾节点
            if (compareAndSetTail(t, node)) {
    
    	// 通过CAS方式将Node添加到AQS队列尾部
                t.next = node;	// CAS成功则原来的AQS尾节点的后节点指向当前线程Node
                return t;
            }
        }
    }
}

eq メソッドのプロセス全体は、AQS キューを初期化することです。まず、キューにノード データがないことを確認してから、CAS を通じて現在のスレッド ノードをキューの先頭に追加します。この時点で、キューにはノード データのみが含まれています1 つのノードなので、Node の前のノードを自分自身にポイントし、CAS を使用してキューの末尾を Node に設定し、末尾の後ろのノードを Node にポイントします。

このとき、CAS が失敗すると、他のスレッドが初期化を完了しており、CAS を介してキューの末尾にノードが追加され、キューの末尾ノードの後ろのノードがそのノードを指します。現時点では、この一連の操作はキューの最後尾が正常に追加されるまでスピンし、その後スピンが終了します。

AbstractQueuedSynchronizer.acquireQueued メソッド

final boolean acquireQueued(final Node node, int arg) {
    
    
    boolean failed = true;	// 失败标识
    try {
    
    
        boolean interrupted = false;	// 线程终端标识
        for (;;) {
    
    
            final Node p = node.predecessor();	// 获取当前线程Node的前节点
            // 如果Node的前节点为AQS头部head即Node处于队列最前端,每次只有队列最前端的Node才能后去抢占锁,直到抢占成功
            if (p == head && tryAcquire(arg)) {
    
    	
                setHead(node);	// 线程抢占锁成功则将将该线程Node从队列中移除
                p.next = null; 	// 线程Node被设为head,原来的head后节点设为null使其能被GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
    
    
        if (failed)
            cancelAcquire(node);
    }
}
private void setHead(Node node) {
    
    
    head = node;
    node.thread = null;
    node.prev = null;
}

AcquireQueued メソッド全体のプロセスは大まかに次のとおりです。

  1. 現在のスレッド ノードの prev を取得します。 prev がヘッド ノードの場合、ロックをめぐって競合し、tryAcquire メソッドを呼び出してロックを取得します。
  2. ノードがロックを正常に取得した場合は、ノードをヘッドとして設定し、元の初期化ヘッド ノードを削除します。
  3. ロックの取得に失敗した場合は、waitStatusに基づいてスレッドを一時停止する必要があるかどうかを判断し、スレッドを一時停止した後、cancelAcquireによりロックの取得操作をキャンセルします。

おすすめ

転載: blog.csdn.net/weixin_44947701/article/details/125191641