Java並行プログラミング(4) スレッド同期【AQS|ロック】

概要

Java では、複数のスレッドが特定のパブリック リソースにアクセスするときに、ロックを使用してリソース アクセスのセキュリティを確保できます。Java は 2 つのロック方法を提案しています

  • 1 つ目は、上で説明したように、キーワード synchronized を使用したロックです。synchronized の基礎となる層は、実行のために JVM によってホストされており、Java 1.6 以降では多くの最適化 (バイアス ロック、スピン、軽量ロック) が行われています。非常に使いやすく、パフォーマンスも非常に優れているため、必要がない場合には同期操作に synchronized を使用することをお勧めします。
  • 2つ目は、この記事で紹介するjava.util.concurrentパッケージ配下のLockによるロックです( lockはCAS+spinを多用します。そのため、CASの特性上、次のような場合にはlockを使用することをお勧めします)ロック競合が少ない)

AQS

概要

  • AQS の正式名は AbstractQueuedSynchronizer で、抽象キュー シンクロナイザーと訳されます。
  • AQS の基礎となるデータ構造は、揮発性の変更された状態とノードの双方向キューです。
  • Lock の実装クラスには、ReentrantLock、ReadLock、および WriteLock が含まれます。これらはすべて、ロック リソースを取得または解放するための AQS に基づいています。

内部構造

ソース コードによれば、AQS が揮発性状態と CLH (FIFO) 双方向キューを維持していることがわかります。

stateは volatile によって変更された int 型のミューテックス変数で、state= 0 はリソースを使用しているタスクスレッドがないことを意味し、state>= 1 はスレッドがすでにロックリソースを保持していることを意味します。CLH キューは、内部クラス Node によって維持される FIFO キューです。

実施原則

スレッドがロックリソースを取得すると、まず状態が0(ロックフリー状態)かどうかを判定し、0であれば状態を1に更新します。このプロセスで、複数のスレッドが状態更新操作を同時に実行すると、スレッドの安全性の問題が発生します。したがって、AQS の最下層は CAS メカニズムを採用して、相互に排他的な変数状態更新のアトミック性を保証しますロックを取得していないスレッドは、Unsafe クラスの park メソッドによってブロックされ、ブロックされたスレッドは先入れ先出しの原則に従って CLH 二重リンク リストに配置されます。ロックを取得したスレッドが解放されると、ロックを取得すると、この二重リンク リストの先頭から開始され、次の待機スレッドを起動してロックを競合します。

公平なロックと不公平なロック

ロック リソースをめぐって競合する場合、フェア ロックは二重リンク リストにブロックされたスレッドがあるかどうかを判断する必要があり、ブロックされている場合はキューに入れて待機する必要があります。不公平なロックの処理方法は、二重リンク リストのキューで待機しているブロックされたスレッドがあるかどうかに関係なく、状態変数を変更してロックを競合しようとします。これは、リンク リストのキューにあるスレッドにとって不公平です。 。

ロックインターフェース

ロック実装クラス

  • JDK8 では、非リエントラント ロックである StampedLock を除き、ReentrantLock、ReentrantReadWriteLock、Synchronized などの他のキーワードはすべてリエントラント ロックです。
  • リエントラント ロックとは、スレッドがミューテックス ロック リソースをプリエンプトし、ロックが解放される前に繰り返しロック リソースを取得できることを意味します。再入可能回数を記録するだけでよく、状態は 1 ずつ増分されます。
  • ロックは実際に、AQS の状態を更新することでロック保持状況を制御します。

ロック方式

// 尝试获取锁,获取成功则返回,否则阻塞当前线程
void lock();
// 尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常
void lockInterruptibly() throws InterruptedException;
// 尝试获取锁,获取锁成功则返回true,否则返回false
boolean tryLock();
// 尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
Condition newCondition();

リエントラントロック

  • ソースコードを見ると、ロック関数を実装するキーメンバ変数Sync typeがAQSを継承していることがわかります。
  • Sync には、ReentrantLock に 2 つの実装クラスがあります。NonfairSync 公正ロック タイプと FairSync 不公平ロック タイプです。
  • ReentrantLock はデフォルトで不公平なロック実装ですが、インスタンス化するときに公平なロックまたは不公平なロックを指定できます。

ReentrantLock取得ロック処理

//.lock()调用的是AQS的acquire()
public void lock() {
    sync.acquire(1);
}

public final void acquire(int arg) {
    //tryAcquire:会尝试通过CAS获取一次锁。
    //addWaiter:将当前线程加入双向链表(等待队列)中
    //acquireQueued:通过自旋,判断当前队列节点是否可以获取锁
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

//---------------------非公平锁尝试获取锁的过程---------------------
protected final boolean tryAcquire(int acquires) {
	// AQS的nonfairTryAcquire()方法
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
	// 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取state
    int c = getState();
    if (c == 0) {
    	// 目前没有线程获取锁,通过CAS(乐观锁)去修改state的值
        if (compareAndSetState(0, acquires)) {
        	// 设置持有锁的线程为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 锁的持有者是当前线程(重入锁)
    else if (current == getExclusiveOwnerThread()) {
    	// state + 1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

//---------------------当前线程加入双向链表的过程---------------------
private Node addWaiter(Node mode) {
    Node node = new Node(mode);

    for (;;) {
    	// 获取末位节点
        Node oldTail = tail;
        if (oldTail != null) {
        	// 当前节点的prev设置为原末位节点
            node.setPrevRelaxed(oldTail);
            // CAS确保在线程安全的情况下,将当前线程加入到链表的尾部
            if (compareAndSetTail(oldTail, node)) {
            	// 原末位节点的next设置为当前节点
                oldTail.next = node;
                return node;
            }
        } else {
        	// 链表为空则初始化
            initializeSyncQueue();
        }
    }
}

//---------------------首节点自旋过程---------------------
final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            final Node p = node.predecessor();
            // 首节点线程去尝试竞争锁
            if (p == head && tryAcquire(arg)) {
            	// 成功获取到锁,从首节点移出(FIFO)
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}

ReentrantLockロック解除処理

ロックの解放の本質は、AQS の状態値 State を徐々にデクリメントすることです。

//.unlock()调用AQS的release()方法释放锁资源
public void unlock() {
    sync.release(1);
}

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

protected final boolean tryRelease(int releases) {
	// 获取状态
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 修改锁的持有者为null
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

リエントラント読み取り書き込みロック

  • ReentrantReadWriteLock 読み取り/書き込みロックは、それぞれ読み取りロックまたは書き込みロックを取得できます。つまり、データの読み取り操作と書き込み操作を分離できます
  • writeLock(): 書き込みロックを取得します。
    • writeLock().lock(): 書き込みロックをロックします。
    • writeLock().unlock(): 書き込みロックのロックを解除します。
  • readLock(): 読み取りロックを取得します。
    • readLock().lock(): 読み取りロックをロックします。
    • readLock().unlock(): 読み取りロックのロックを解除します。
  • 読み取りロックは共有モードを使用し、書き込みロックは排他モードを使用しますつまり、書き込みロックがない場合は、複数のスレッドが読み取りロックを同時に保持できますが、書き込みロックがある場合は、書き込みロックを取得したスレッドを除いて、他のスレッドは読み取りロックを取得できません。読み取りロックがある場合、書き込みロックは取得できません
  • キャッシュなど、読み取りを増やし書き込みを減らすアプリケーション シナリオに適しています。

状態

概要 

  • 条件は、await と singalAll() によるスレッドのブロックとウェイクアップを実装するスレッド通信メカニズムでもあります。
  • 基礎となるデータ構造は、AQS の Node クラスを再利用するキューで、先頭ノードのないリンク リストによって実装されます。
  • Await 実装原則: 他のスレッドが signal または signalAll を呼び出して待機キューのヘッド ノードを同期キューに移動し、スピンを通じてロックを取得する機会を与えるまで、LockSupport.park を通じて現在のスレッドを待機ブロッキング状態にします。
  • signal/signalAll: 待機キューのヘッド ノードを同期キューに移動し、LockSupport.unpark を通じてスレッドをウェイクアップします。
  • オブジェクトの待機/通知メカニズムとの比較
    • 条件は割り込みに応答しないことをサポートしますが、オブジェクトはサポートできません
    • ロックは複数の条件待機キューをサポートできますが、オブジェクトは 1 つだけサポートできます。
    • 条件では待機のタイムアウトを設定できますが、オブジェクトでは設定できません
  • プロデューサーとコンシューマーの問題は、Lock+Condition を通じて実現できます (同時実行の実践の章で関連する例が後で説明されます)。

コンディション練習

package com.bierce;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * Lock + Condition: 实现线程按序打印
 * 案例:开启3个线程,id分别为A、B、C,并打印10次,而且按顺序交替打印如:ABCABCABC...
 */
public class TestCondition {
    public static void main(String[] args) {
        PrintByOrderDemo print = new PrintByOrderDemo();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {// 打印10次
                print.loopA(i);
            }
        },"A").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                print.loopB(i);
            }
        },"B").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                print.loopC(i);
            }
        },"C").start();
    }
}
class PrintByOrderDemo{
    private int number = 1; //当前正在执行线程的标记
    private Lock lock = new ReentrantLock();
    private Condition ConditionA = lock.newCondition();
    private Condition ConditionB = lock.newCondition();
    private Condition ConditionC = lock.newCondition();

    public void loopA(int totalLoop){
        lock.lock();
        try {
            if ( number != 1){ //判断当前是否打印A
                ConditionA.await();
            }
            for (int i = 1; i <= 1; i++) {
                System.out.print(Thread.currentThread().getName()); //打印A
            }
            //唤醒其他线程
            number = 2;
            ConditionB.signal();
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void loopB(int totalLoop){
        lock.lock();
        try {
            if ( number != 2){ //判断当前是否打印B
                ConditionB.await();
            }
            for (int i = 1; i <= 1; i++) {
                System.out.print(Thread.currentThread().getName()); //打印B
            }
            //唤醒其他线程
            number = 3;
            ConditionC.signal();
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void loopC(int totalLoop){
        lock.lock();
        try {
            if ( number != 3){ //判断当前是否打印C
                ConditionC.await();
            }
            for (int i = 1; i <= 1; i++) {
                System.out.print(Thread.currentThread().getName()); //打印C
            }
            //唤醒其他线程
            number = 1;
            ConditionA.signal();
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

おすすめ

転載: blog.csdn.net/qq_34020761/article/details/132234785
おすすめ