第 13 章 Java ロック - 上級

AQSの原則

概要

  • 正式名は AbstractQueuedSynchronizer で、JUC のフレームワークであり、JUC システム全体の基礎 阻塞式锁です。相关的同步器工具
    • ロック、ユーザーをロックすることを目的としています (プログラマーがロックを操作するための使用法レイヤー API を定義し、実装の詳細を非表示にし、呼び出すだけで使用できます)
      • ロックするとブロッキングが発生し、ブロッキングが発生した場合はキューイングが必要になります。キューイングを実現するにはキューが必要です。
    • シンクロナイザーはロック実装者です (統一仕様を提案し、ロックの実装を簡素化した Java 同時実行マスター Douglee など)、同期状態管理のシールド、スレッドのキューイングと通知のブロック、ウェイクアップ メカニズムなど)

画像

  • state 属性を使用して、リソースの状態 (排他モードと共有モードに分けられる) を表します。サブクラスは、この状態を維持する方法を定義し、ロックの取得と解放の方法を制御する必要があります。
    • getState - 状態を取得する
    • setState - 状態を設定する
    • CompareAndSetState - cas メカニズムが状態を設定します
    • 排他モードでは 1 つのスレッドのみがリソースにアクセスできますが、共有モードでは複数のスレッドがリソースにアクセスできます。
  • リソース取得スレッドのキューイング作業は、モニターの EntryList と同様の、組み込み CLH (FIFO) キューのバリアントを通じて完了します。
    • リソースを捕捉する各スレッドを Node ノードにカプセル化し、ロック割り当てを実装します。
    • CLH: Craig、Landin、Hagersten のキューは一方向リンク リストですが、AQS のキューは CLH バリアントの仮想双方向キュー FIFO です。
  • 条件変数 (Condition) は、モニターの WaitSet と同様に、待機およびウェイクアップ メカニズムを実装し、複数の条件変数をサポートするために使用されます。

AQS が達成すべき機能目標

  • ブロッキング バージョンはロックを取得し、非ブロッキング バージョンはロックの取得を試みます tryAcquire
  • 取得ロックタイムアウト機構
  • 割り込みによるキャンセル機構
  • 排他的な仕組みと共有の仕組み
  • 条件不成立時の待機機構

AQS の中身

画像-20230701131759000 画像-20230701140839181
  • AQS の同期状態 State メンバー変数は、可視性を確保するために volatile で変更されます。
    • 銀行事務処理業務の受付窓口状況に類似
      • 0 は誰もいないことを意味し、フリーステータスで処理できます
      • 1 以上、誰かがウィンドウを占有し、出発を待っています
  • AQSのCHLキュー
    • 銀行の顧客エリアで待っている顧客のたとえ
  • 内部クラス Node (Node クラスは AQS クラス内にあります)
    • volatile int waitStatus はノードの待機ステータスを表します
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;
        Node nextWaiter;
}
ここに画像の説明を挿入します

AQS同期キューの基本構造

画像-20230701145159588

AQS が JUC コンテンツの最も重要な基盤である理由

ReentrantLock|CountDownLatch|ReentrantReadWriteLock|Semaphore、これらの基本原則はすべて AQS です

画像-20230701130715029

ReentrantLockの基本的な使い方

同期と比較すると以下のような特徴があります

  • 中断可能
  • タイムアウトを設定できる
  • フェアロックに設定可能
  • 複数の条件変数をサポート

同期と同様に、どちらも再入可能をサポートしています。

基本的な文法

// 获取锁
reentrantLock.lock();
try {
    
    
	// 临界区
} finally {
    
    
	// 释放锁
	reentrantLock.unlock();
}

対応機能と基本的な使い方

リエントラント

再入可能とは、同じスレッドが初めてロックを取得した場合、そのスレッドがロックの所有者であるため、再度ロックを取得する権利があることを意味します。非再入可能ロックの場合は、ロックを取得するとき2回目もロックでブロックされます

public class reentrantLockDemo {
    
    
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
    
    
        method1();
    }
    public static void method1() {
    
    
        lock.lock();
        try {
    
    
            System.out.println("execute method1");
            method2();
        } finally {
    
    
            lock.unlock();
        }
    }
    public static void method2() {
    
    
        lock.lock();
        try {
    
    
            System.out.println("execute method2");
            method3();
        } finally {
    
    
            lock.unlock();
        }
    }
    public static void method3() {
    
    
        lock.lock();
        try {
    
    
            System.out.println("execute method3");
        } finally {
    
    
            lock.unlock();
        }
    }
}
execute method1
execute method2
execute method3

中断可能

public class reentrantLockDemo {
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"启动...");
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println(Thread.currentThread().getName()+"等锁的过程中被打断");
                return;
            }
            try {
                System.out.println(Thread.currentThread().getName()+"获得了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");
        lock.lock();
        System.out.println(Thread.currentThread().getName()+"获得了锁");
        t1.start();
        try {
           TimeUnit.SECONDS.sleep(1);
            t1.interrupt();
            System.out.println(Thread.currentThread().getName()+"执行打断");
        } finally {
            lock.unlock();
        }
    }
}
main获得了锁
t1启动...
main执行打断
t1等锁的过程中被打断
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at com.lsc.day12.reentrantLockDemo.lambda$main$0(reentrantLockDemo.java:20)
	at java.lang.Thread.run(Thread.java:748)
  • なお、非割込みモードの場合、割込みを使用しても待機は中断されません。
public class reentrantLockDemo {
    
    
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t1 = new Thread(() -> {
    
    
            System.out.println(Thread.currentThread().getName()+"启动...");
				
            lock.lock();
            try {
    
    
                System.out.println(Thread.currentThread().getName()+"获得了锁");
            } finally {
    
    
                lock.unlock();
            }
        }, "t1");
        lock.lock();
        System.out.println(Thread.currentThread().getName()+"获得了锁");
        t1.start();
        try {
    
    
           TimeUnit.SECONDS.sleep(1);
            t1.interrupt();
            System.out.println(Thread.currentThread().getName()+"执行打断");
        } finally {
    
    
            lock.unlock();
        }
    }
}
main获得了锁
t1启动...
main执行打断
t1获得了锁

ロックタイムアウト

    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
    
    
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
    
    
            System.out.println("启动...");
            try {
    
    
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
    
    
                    System.out.println("获取等待 1s 后失败,返回");
                    return;
                }
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            try {
    
    
                System.out.println("获得了锁");
            } finally {
    
    
                lock.unlock();
            }
        }, "t1");
        lock.lock();
        System.out.println("获得了锁");
        t1.start();
        try {
    
    
           Thread.sleep(2000);
        } finally {
    
    
            lock.unlock();
        }
    }
获得了锁
启动...
获取等待 1s 后失败,返回

フェアロック

ReentrantLock のデフォルトは不公平です

 public static void main(String[] args) throws InterruptedException {
    
    
        ReentrantLock lock = new ReentrantLock(false);
        lock.lock();
        for (int i = 0; i < 500; i++) {
    
    
            new Thread(() -> {
    
    
                lock.lock();
                try {
    
    
                    System.out.println(Thread.currentThread().getName() + " running...");
                } finally {
    
    
                    lock.unlock();
                }
            }, "t" + i).start();
        }
        // 1s 之后去争抢锁
        Thread.sleep(1000);
        new Thread(() -> {
    
    
            System.out.println(Thread.currentThread().getName() + " start...");
            lock.lock();
            try {
    
    
                System.out.println(Thread.currentThread().getName() + " running...");
            } finally {
    
    
                lock.unlock();
            }
        }, "强行插入").start();
        lock.unlock();
    }
}
t39 running...
t40 running...
t41 running...
t42 running...
t43 running...
强行插入 start...
强行插入 running...
t44 running...
t45 running...
t46 running...
t47 running...
t49 running...

改为公平锁后

t465 running...
t464 running...
t477 running...
t442 running...
t468 running...
t493 running...
t482 running...
t485 running...
t481 running...
强行插入 running...
  • フェア ロックは一般に不要であり、同時実行性が低下します。これについては、後で原理を分析するときに説明します。

条件変数

同期には条件変数もあり、原理を説明したときのwaitSetラウンジですが、条件が満たされない場合はwaitSetに入って待機します。

  • ReentrantLock の条件変数は、複数の条件変数をサポートするという点で synchronized よりも強力です。
  • 同期済みとは、条件を満たさないスレッドがラウンジでメッセージを待っていることを意味します
  • ReentrantLockはタバコ待ち専用ラウンジ、朝食待ち専用ラウンジなど複数のラウンジに対応しており、起床時にラウンジを押すことで起床することも可能です。

使用上の重要なポイント:

  • 待機する前にロックを取得する必要がある
  • await が実行されると、ロックが解放され、conditionObject が入力されて待機します。
  • await スレッドはウェイクアップ (または中断、またはタイムアウト) され、再びロックを獲得するために競合します。
  • ロック競合が成功した後、待機後に実行が続行されます。

重写之前wait/notify实现的需求

public class ConditionDemo {
    
    
    static ReentrantLock lock = new ReentrantLock();
    static Condition waitCigaretteQueue = lock.newCondition();
    static Condition waitBreakfastQueue = lock.newCondition();
    static volatile boolean hasCigrette = false;
    static volatile boolean hasBreakfast = false;

    public static void main(String[] args) throws InterruptedException {
    
    
        new Thread(()->{
    
    
            try {
    
    
                lock.lock();
                while (!hasBreakfast){
    
    
                    try {
    
    
                        waitBreakfastQueue.await();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                System.out.println("得到了他的早餐");
            }finally {
    
    
                lock.unlock();
            }
        },"t1").start();
        new Thread(()->{
    
    
            try {
    
    
                lock.lock();
                while (!hasCigrette){
    
    
                    try {
    
    
                        waitCigaretteQueue.await();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                System.out.println("得到了他的烟");
            }finally {
    
    
                lock.unlock();
            }
        },"t2").start();
        TimeUnit.SECONDS.sleep(1);
        sendBreakfast();
        TimeUnit.SECONDS.sleep(1);
        sendCigarette();
    }
    private static void sendCigarette() {
    
    
        lock.lock();
        try {
    
    
            System.out.println("送烟来了");
            hasCigrette = true;
            waitCigaretteQueue.signal();
        } finally {
    
    
            lock.unlock();
        }
    }
    private static void sendBreakfast() {
    
    
        lock.lock();
        try {
    
    
            System.out.println("送早餐来了");
            hasBreakfast = true;
            waitBreakfastQueue.signal();
        } finally {
    
    
            lock.unlock();
        }
    }
}

  • 以前の同期実装では両方のスレッドが待機キューに入れられていましたが、今回は異なるニーズを異なる待機キューに入れて、対応するスレッドを正確に起動できるようにしました。

ReentrantLockの不当ロック原理の解析

ReentranLock の不公平なロックを使用して AQS を解析する

施工方法

public ReentrantLock() {
    
    
	sync = new NonfairSync();//默认是不公平
}
  • NonfairSync は AQS から継承します
画像-20230701150703939

ロック方式

public void lock() {
    
    
     sync.lock();
}
  • シンクロナイザーのロックメソッドが呼び出されます。
 //非公平的锁的实现
final void lock() {
    
    
    if (compareAndSetState(0, 1))
       setExclusiveOwnerThread(Thread.currentThread());
    else
       acquire(1);
}
//公平锁的实现
final void lock() {
    
    
    acquire(1);
}
public final void acquire(int arg) {
    
    
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}
  • 不公平なロックに対応して、公平なロックよりも if 判定が 1 つ多く、CAS 操作を使用して現在の AQS の State 属性の値を設定します。
    • Sync は AQS クラスを継承するため、AQS の CompareAndSetState メソッドを呼び出します。
    • 初期値は0で占有可能、0より大きい場合は占有不可(0より大きいのはリエントラントロックだから)
  • したがって、初めてロックし、競合がない場合は、compareAndSetState(0, 1) を実行します。
画像-20230701152249849
  • 初めてのロックではなく、競合がある場合は、acquire(1) が実行されます。acquire には 3 つのフローがあります。
  • !tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
    • tryAcquire - ロックを試みます
    • addWaiter—キューに参加します (CLH)
    • キューに登録された取得
    • ここで && 機能が使用されているのは、false が発生すると、後続の条件が実行されなくなるためです。

取得してみる

protected boolean tryAcquire(int arg) {
   throw new UnsupportedOperationException();
}
  • tryAcquire は AQS (親クラス) には実装されていませんが、サブクラスの実装 (同期クラス Sync の実装) に依存しています。
  • ここではテンプレートメソッドのデザインパターンを使用します。
 protected final boolean tryAcquire(int acquires) {
    
    
      return nonfairTryAcquire(acquires);
 }
 final boolean nonfairTryAcquire(int acquires) {
    
    
      final Thread current = Thread.currentThread();
      int c = getState();
      if (c == 0) {
    
    
          if (compareAndSetState(0, acquires)) {
    
    
              etExclusiveOwnerThread(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;
 }
  • c==0 の場合は、現在のロックが占有されていないことを意味するため、ロックを試みます。成功すると、tryAcquire は true を返します。それ以外の場合は、false となり、addWaiter メソッドとacquireQueued メソッドは実行されません。
    • ブロッキング キューにはすでにブロックされているスレッドが存在する可能性がありますが、この時点ではブロッキング キューに入ろうとしている新しいスレッドが正常に実行され、ロックが正常に取得されます。
    • 公平なロックが実装されている場合、!hasQueuedPredecessors() による判定が行われますが、前に他の待機スレッドがある場合、compareAndSetState は実行されずにロックを取得します。
  • else if を入力すると、現在ロックを占有しているスレッドが存在することを示します。現在ロックを占有しているスレッドが、現在ロックしようとしているスレッドと同じである場合は、ロックを再入力するロジック (つまり、state+1) が使用されます。
  • どちらも満たされない場合は、false を返し、addWaiter メソッドとacquireQueued メソッドの実行を続行します。

追加ウェイター

private Node addWaiter(Node mode) {
    
    
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
    
    
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
    
    
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
}
  • addWaiter(Node.EXCLUSIVE)
    • EXCLUSIVE は、スレッドが排他的にロックを待機していることを示します
  • pred != null——この判定は、キューが初期化されているかどうかを判定するために使用されます。null に等しい場合は初期化されていることを意味し、null に等しくない場合は初期化されていることを意味します。

当未初始化时,执行enq方法

   private Node enq(final Node node) {
    
    
        for (;;) {
    
    
            Node t = 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;
                }
            }
        }
    }
  • このメソッドを入力すると、現在のキューが初期化されていないことを示すため、最初のループ中に if ステートメントに入る必要があります。これは、対応してセンチネル ノードを作成し、ヘッド ノードとテール ノードがこれを指すようにすることを意味します。
    • このセンチネル ノードには対応するスレッドがなく、ステータスも 0 です。

各ノードは先行ノードの ws ステータスを SIGNAL に設定する必要があるため、先行ノードが必要であり、この先行ノードが実際に現在ロックを保持しているノードになります。

問題は、最初のノードをどうするかという境界の問題があることです。先行ノードはありません。次に、偽物を作成します。

このため、ダミー ノードが作成されます。

まとめると、各ノードは前ノードの ws ステータス (このステータスはデータの整合性を確保するためのもの) を設定する必要があり、最初のノードには前ノードがないため、仮想ノードを作成する必要があります。

  • このループに 2 回目に入るときは、else 判定ステートメントを使用する必要があります。つまり、待機中の Node ノードを実際にキューに追加します。その後、ループは戻り値を返すため終了します。
画像-20230701161738068

初始化则执行if内语句

if (pred != null) {
    
    
    node.prev = pred;
   if (compareAndSetTail(pred, node)) {
    
    
        pred.next = node;
        return node;
    }
 }
  • 普通にノードをキューの最後に置くだけです

キューに登録された取得

 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);
        }
    }
  • acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
  • acquireQueued は無限ループでロックの取得を継続的に試行します。失敗するとパーク ブロッキングに入ります。まず、node.predecessor() を通じて現在のノードの先行ノードを取得します。
    • 自分が先頭(2 位)の場合は、再度 tryAcquire を実行してロックを取得しようとしますが、当然この時点では state は 1 のままなので失敗します。
  • 失敗した場合は、 shouldParkAfterFailedAcquire ロジックに入り、先行ノード (head) の waitStatus を -1 に変更し、今度は false を返します。 false を返すと、後続の parkAndCheckInterrupt が実行されないためです。
    • -1 は、リソースの準備ができており、ロックが解放されるのを待っていることを示します。
画像-20230701165100811
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    
    
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
    
    
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
    
    
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
    
    
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
  • shouldParkAfterFailedAcquire を実行した後、acquireQueued ループに戻り、再度 tryAcquire を実行してロックの取得を試みますが、当然この時点では状態は 1 のままなので失敗します。
  • 再度 shouldParkAfterFailedAcquire に入ると、その先行ノードの waitStatus がすでに -1 であるため、今回は true を返します。
  • parkAndCheckInterrupt、Thread-1 park (灰色で表示) を入力します。
画像-20230701165638756
 private final boolean parkAndCheckInterrupt() {
    
    
        LockSupport.park(this);
        return Thread.interrupted();
}
  • ここでの parkAndCheckInterrupt の実行では、LockSupport.park(this) を使用して、スレッドの実際のブロックを実現します。

複数のスレッドがロックを競合して失敗すると、次のようになります。

画像-20230701165943091

知らせ

  • アンパークが必要かどうかは、このノードの waitStatus ではなく、現在のノードの先行ノードの waitStatus == Node.SIGNAL によって決まります。

lock() は許可を取得します

public void unlock() {
    
    
   sync.release(1);
}
 public final boolean release(int arg) {
    
    
        if (tryRelease(arg)) {
    
    
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
}
protected boolean tryRelease(int arg) {
    
    
   throw new UnsupportedOperationException();
}
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;
}
  • ここでもテンプレート メソッドの設計パターンが採用されており、親クラスがそれを呼び出し、実装はサブクラスに任せます。
  • tryRelease の場合、現在の状態が 1 で、ロック解除を実行しているスレッドが現在ロックを占有しているスレッドである場合、現在ロックを占有しているスレッドも null に設定し、状態を 0 に設定して true を返します。
    • リエントラントロックのため、今回はロック解除が成功しない可能性があり、つまり false が返されますが、成功しても失敗しても状態は -1 になりますが、状態が 0 の場合にのみスレッドが占有されます。ロックは null に設定されます。
  • tryRelease は true を返し、現在のキューは null ではなく、head の waitStatus = -1 で、unparkSuccessor プロセスに入ります。
private void unparkSuccessor(Node node) {
    
    
        int ws = node.waitStatus; //获得哨兵节点的状态
        if (ws < 0)
            //将哨兵节点的状态设为0
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;//获得哨兵节点的下一个线程
        if (s == null || s.waitStatus > 0) {
    
    
            //如果这个节点为null,或者状态码大于0,也就是1,说明线程请求锁的请求取消了,就需要将对应的节点移除
            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);
    }

なぜ不公平なのか

unparkSuccessor では、acquireQueued プロセスを確認してみましょう

画像-20230701173045196ロックが成功した場合 (競合なし)、

  • exclusiveOwnerThread を Thread-1、状態 = 1 に設定します。
  • ヘッドはスレッド 1 が先ほど配置されていたノードを指し、ノードはスレッドをクリアします。
  • 元のヘッドはリンク リストから切り離されているため、ガベージ コレクションが可能です。

この時点で競合する他のスレッドがある場合 (不公平な兆候)、たとえば、この時点で Thread-4 が到着します。

画像-20230701173329039
  • 万が一、スレッド 4 が再び引き継いだ場合、
  • Thread-4 は exclusiveOwnerThread に設定され、状態 = 1
  • Thread-1 は、acquireQueued プロセスに再度入りますが、ロックの取得に失敗し、パーク ブロッキングに再度入ります。

ロックされたソースコードの概要

// Sync 继承自 AQS
static final class NonfairSync extends Sync {
    
    
	 private static final long serialVersionUID = 7316153563782823691L;
    final void lock() {
    
    
		// 首先用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示获得了独占锁
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
			// 如果尝试失败,进入 ㈠
            acquire(1);
    }
    // ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
    public final void acquire(int arg) {
    
    
			// ㈡ tryAcquire
        if (!tryAcquire(arg) &&// 当 tryAcquire 返回为 false 时, 先调用 addWaiter ㈣, 接着 acquireQueued ㈤
           acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
        ) {
    
    
            selfInterrupt();
        }
    }
    // ㈡ 进入 ㈢
    protected final boolean tryAcquire(int acquires) {
    
    
        return nonfairTryAcquire(acquires);
    }
    // ㈢ Sync 继承过来的方法, 方便阅读, 放在此处
    final boolean nonfairTryAcquire(int acquires) {
    
    
        final Thread current = Thread.currentThread();
        int c = getState();
		  // 如果还没有获得锁
        if (c == 0) {
    
    
				// 尝试用 cas 获得, 这里体现了非公平性: 不去检查 AQS 队列
            if (compareAndSetState(0, acquires)) {
    
    
                setExclusiveOwnerThread(current);
                return true;
            }
        }
		 // 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入
        else if (current == getExclusiveOwnerThread()) {
    
    
				// state++
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
		// 获取失败, 回到调用处
        return false;
    }
    // ㈣ AQS 继承过来的方法, 方便阅读, 放在此处
    private Node addWaiter(Node mode) {
    
    
       // 将当前线程关联到一个 Node 对象上, 模式为独占模式
    	Node node = new Node(Thread.currentThread(), mode);
    	Node pred = tail;
       // 如果 tail 不为 null, cas 尝试将 Node 对象加入 AQS 队列尾部
		if (pred != null) {
    
    
        	node.prev = pred;
        	if (compareAndSetTail(pred, node)) {
    
    
				// 双向链表
        		pred.next = node;
        		return node;
       		 }
       }
		// 尝试将 Node 加入 AQS, 进入 ㈥
        enq(node);
        return node;
     }
	// ㈥ AQS 继承过来的方法, 方便阅读, 放在此处
		private Node enq(final Node node) {
    
    
        for (;;) {
    
    
     	 	Node t = tail;
        	if (t == null) {
    
    
				// 还没有, 设置 head 为哨兵节点(不对应线程,状态为 0)
        		if (compareAndSetHead(new Node())) {
    
    
        			tail = head;
        		}
        	} else {
    
    
				// cas 尝试将 Node 对象加入 AQS 队列尾部
        		node.prev = t;
        		if (compareAndSetTail(t, node)) {
    
    
        			t.next = node;
        			return t;
       		 	}
        	}
        }
      }
	// ㈤ AQS 继承过来的方法, 方便阅读, 放在此处
	final boolean acquireQueued(final Node node, int arg) {
    
    
        boolean failed = true;
        try {
    
    
        	boolean interrupted = false;
        	for (;;) {
    
    
				final Node p = node.predecessor();
				// 上一个节点是 head, 表示轮到自己(当前线程对应的 node)了, 尝试获取
        	  if (p == head && tryAcquire(arg)) {
    
    
					// 获取成功, 设置自己(当前线程对应的 node)为 head
        			setHead(node);
					// 上一个节点 help GC
        			p.next = null;
        			failed = false;
					// 返回中断标记 false
        			return interrupted;
             if (shouldParkAfterFailedAcquire(p, node)// 判断是否应当 park, 进入 ㈦
        		 &&parkAndCheckInterrupt()// park 等待, 此时 Node 的状态被置为 Node.SIGNAL ㈧) 
              {
    
    
        			interrupted = true;
        		}
        	}
        } finally {
    
    
        	if (failed)
        		cancelAcquire(node);
       		}
        }
		// ㈦ AQS 继承过来的方法, 方便阅读, 放在此处
		private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    
    
		  // 获取上一个节点的状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL) {
    
    
			// 上一个节点都在阻塞, 那么自己也阻塞好了
        	return true;
         }
		  // > 0 表示取消状态
        if (ws > 0) {
    
    
			// 上一个节点取消, 那么重构删除前面所有取消的节点, 返回到外层循环重试
        	do {
    
    
        		node.prev = pred = pred.prev;
       		} while (pred.waitStatus > 0);
        		pred.next = node;
       	 } else {
    
    
			// 这次还没有阻塞
			// 但下次如果重试不成功, 则需要阻塞,这时需要设置上一个节点状态为 Node.SIGNAL
        	compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
       }
		// ㈧ 阻塞当前线程
		private final boolean parkAndCheckInterrupt() {
    
    
        	LockSupport.park(this);
        	return Thread.interrupted();
        }
}

ソースコードのロックを解除する

// Sync 继承自 AQS
static final class NonfairSync extends Sync {
    
    
    // 解锁实现
    public void unlock() {
    
    
        sync.release(1);
     }
    // AQS 继承过来的方法, 方便阅读, 放在此处
    public final boolean release(int arg) {
    
    
			// 尝试释放锁, 进入 ㈠
        if (tryRelease(arg)) {
    
    
			 // 队列头节点 unpark
            Node h = head;
            if (h != null// 队列不为 null
               h.waitStatus != 0 && // waitStatus == Node.SIGNAL 才需要 unpark
             ) {
    
    
					// unpark AQS 中等待的线程, 进入 ㈡
                unparkSuccessor(h);
            }
            return true;
         }
        return false;
     }
    // ㈠ Sync 继承过来的方法, 方便阅读, 放在此处
    protected final boolean tryRelease(int releases) {
    
    
		 // state--
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
		  // 支持锁重入, 只有 state 减为 0, 才释放成功
        if (c == 0) {
    
    
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }
    // ㈡ AQS 继承过来的方法, 方便阅读, 放在此处
    private void unparkSuccessor(Node node) {
    
    
		// 如果状态为 Node.SIGNAL 尝试重置状态为 0
		// 不成功也可以
       int ws = node.waitStatus;
       if (ws < 0) {
    
    
           compareAndSetWaitStatus(node, ws, 0);
        }
		// 找到需要 unpark 的节点, 但本节点从 AQS 队列中脱离, 是由唤醒节点完成的
    	Node s = node.next;
		// 不考虑已取消的节点, 从 AQS 队列从后至前找到队列最前面需要 unpark 的节点
		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);
        }
 }

読み書きロック

複数のスレッド間では、データを同時に読み取る場合にはスレッド セーフの問題は発生しませんが、データを更新する場合 (追加、削除、または変更する場合) のみスレッド セーフの問題が発生します。両方のシナリオで同じロックが使用されると、パフォーマンスが大幅に低下します。したがって、これにより読み取り/書き込みロックが生成されます。

  • リーダー・ライター・ロック (readers-writer lock) は、英語でその名前が示すように、ロック操作を実行するときに読み取りおよび書き込みの意図を追加で示す必要があり、複数のリーダーは相互に排他的ではありませんが、ライターはどのユーザーとも相互に排他的である必要あります

  • 複数のスレッドが読み取りロック (読み取りデータ) に同時にアクセスする場合、複数のスレッドが読み取りロックにアクセスできます。読み取りロックと読み取りロックは同時に実行され、相互に排他的ではありません。

  • 両方のスレッドが書き込みロックにアクセスする必要がある場合、2 つのスレッドは相互に排他的になります。ロックを正常に取得できるのは 1 つのスレッドだけであり、他のスレッドはブロックされます。

  • 1 つのスレッドが読み取りを行うと、別のスレッドが書き込みを行います (これも相互排他的で、書き込みスレッドが終了した場合にのみ、読み取りスレッドが続行できます)

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

読み取り/書き込みロックは、読み取り操作と書き込み操作を異なる方法で処理します。Java 標準ライブラリには、読み取り/書き込みロックを実装する ReentrantReadWriteLock クラスが用意されています。同期は読み取り/書き込みロックではありません。

  • ReentrantReadWriteLock.ReadLock クラスは読み取りロックを表し、このオブジェクトはロックとロック解除のための lock/unlock メソッドを提供します。
  • ReentrantReadWriteLock.WriteLock クラスは書き込みロックを表し、このオブジェクトはロックとロック解除のための lock/unlock メソッドも提供します。
class DataContainer{
    
    
    private Object date;
    private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock r = rw.readLock();
    private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
    public Object read(){
    
    
        System.out.println("获取读锁");
        r.lock();
        try {
    
    
            System.out.println("读取");
            TimeUnit.SECONDS.sleep(1);
            return date;
        }catch (InterruptedException e) {
    
    
            e.printStackTrace();
            return null;
        }finally {
    
    
            System.out.println("释放读锁");
            r.unlock();
        }
    }
    public Object write(){
    
    
        System.out.println("获取写锁");
        w.lock();
        try {
    
    
            System.out.println("写入");
            TimeUnit.SECONDS.sleep(1);
            return date;
        }catch (InterruptedException e) {
    
    
            e.printStackTrace();
            return null;
        }finally {
    
    
            System.out.println("释放写锁");
            w.unlock();
        }
    }
}
public class ReentrantReadWriteLockDemo {
    
    

    public static void main(String[] args) {
    
    
        DataContainer dataContainer = new DataContainer();
        new Thread(() -> {
    
    
            dataContainer.read();
        }, "t1").start();
        new Thread(() -> {
    
    
            dataContainer.read();
        }, "t2").start();
    }
}
获取读锁
获取读锁
读取
读取
释放读锁
释放读锁
  • 読み取りロックは相互排他的ではありません
public class ReentrantReadWriteLockDemo {
    
    

    public static void main(String[] args) {
    
    
        DataContainer dataContainer = new DataContainer();
        new Thread(() -> {
    
    
            dataContainer.write();
        }, "t1").start();
        new Thread(() -> {
    
    
            dataContainer.read();
        }, "t2").start();
    }
}
获取写锁
获取读锁
写入
释放写锁
读取
释放读锁
  • 読み取りロックと書き込みロックの間の相互排他
  • 書き込み間の相互排他

予防

  • 読み取りロックは条件変数をサポートしません
  • 再入中のアップグレードはサポートされていません。つまり、読み取りロックを保持したまま書き込みロックを取得すると、書き込みロックを取得するまで永続的に待機することになります。
  • 再入時のダウングレードのサポート、つまり、書き込みロックを保持しながら読み取りロックを取得する
r.lock();
try {
    
    
	// ...
	w.lock();
	try {
    
    
		// ...
	} finally{
    
    
		w.unlock();
	}
} finally{
    
    
	r.unlock();
}

おすすめ

転載: blog.csdn.net/qq_50985215/article/details/131511134