JUC スレッド フレームワークの詳細な分析-02、スレッド同期ロック

JUC スレッド フレームワークの詳細な分析 - 02、スレッド同期ロック

juc の開発アーキテクチャが解決する核となる問題は、同時アクセスとデータ セキュリティ操作です.同時アクセス中にロックが適切に制御されないと、デッドロックなどのブロックの問題が発生します.このような欠陥を解決するために、juc はコンセプトを再設計しました.ロックの。

[JUC ロック メカニズム]
➣ JUC ロック メカニズムには次のコンポーネントが含まれます:
➣ コア インターフェイス: Lock、ReadWriteLock;
➣ AQS 抽象クラス: • AbstractOwnableSynchronizer (排他
ロック); • AbstractQueuedLongSynchronizer (64 ビット シンクロナイザー) ➣ ツール: ReentrantLock ミューテックス、ReadWriteLock 読み書きロック、条件制御キュー、LockSupport ブロッキング プリミティブ、Semaphore Semaphore、CountDownLatch ロック、CyclicBarrier フェンス、Exchanger スイッチ、CompletableFuture スレッド バック。


【jucロック機構(java.util.concurrent.locks)】

ここに画像の説明を挿入

前述のインターフェイスとクラスは、すべてのロックの親クラスにすぎません. 次に、さまざまな juc ロック メカニズムの実際の使用に応じて、次の共通ロック処理クラスが提供されます: ReentrantLock ミューテックス、ReadWriteLock 読み書きロック、条件制御キュー、 LockSupport ブロッキング プリミティブ、Semaphore セマフォ、CountDownLatch ロック、CyelicBarrier フェンス、Exchanger スイッチ、CompletableFuture スレッド コールバックなど、一連のロック処理ツールが juc で再提供される根本的な理由は、元の Java ロック メカニズム (同期) にもかかわらずは安全なデータ アクセス メカニズムを提供できますが、その欠点も非常に明白です. すべてのスレッド オブジェクトは 1 つのロックしか享受できません.

[ java.util.concurrent lock の概要]
➣ java.util.concurrent.lock はロックの基本的なサポートを提供します
➣ Lock インターフェイス: 異なるセマンティクス (再入可能性、公平性など) を持つロック規則をサポートします。
➣ いわゆる異なるセマンティクスとは、ロックに「公平なメカニズム ロック」、「不公平なメカニズム ロック」、「リエントラント ロック」などがあることを意味します • 「公平なメカニズム」: 「異なるスレッドがロックを取得するメカニズムが
、 Fair」;
• 「Unfair メカニズム」: 「異なるスレッドがロックを取得するメカニズムが不公平であること」を指します;
• 「Reentrant lock」: 1 つのスレッドが複数回取得できる同じロックと、再入可能ロックの最大値を指します役割は、デッドロックを回避することです。
➣ ReadWriteLock インターフェイスは、Lock と同様の方法で、リーダーが共有し、ライターが排他的に使用できるいくつかのロックを定義します。
➣ 条件: インターフェースは、ロックに関連付けることができる条件変数を記述します (Object クラスの wait() メソッドも同様です)。

[フェアロックの核となる概念]
➣ AbstractQueuedSynchronizer: javaで「ロック」を管理する抽象クラスで、このクラスにはロックの公開メソッドが多数実装されています。AbstractQueuedSynchronizer は排他ロック (ReentrantLock など)
➣ AbstractQueuedSynchronizer カテゴリ:
➣ 排他ロック: ある時点で 1 つのスレッド ロックによってのみ占有できるロック. ロックの取得メカニズムによって、「フェア ロック」と「アンフェアロック」。公平なロックは、スレッドが CLH を通過するのを待つという先着順の規則に従って公平にロックを取得することです。不公平なロックとは、スレッドがロックを取得しようとすると、関係なく直接ロックを取得することを意味します。 CLH 待機キューの。
➣ 共有ロック: 同時に複数のスレッドが所有でき、共有できるロック
➣ CLH キュー (Craig、Landin、および Hagersten ロック): CLH ロックは、スケーラブルで、
高性能で、公平なスピン ロックであり、リンクされたリスト、アプリケーション スレッドはローカル変数でのみスピンし、先行の状態を継続的にトレーニングし、
先行がロックを解放したことが判明した場合はサブスピンを終了します。
➣ CAS 方式 (Compare And Swap) : アトミックな操作方法である比較交換方式、つまり、CAS で操作されるデータをアトミックに実行します。

[ CLH ロック ---- デッドロック問題を解決する]

ここに画像の説明を挿入

排他ロック: ReentrantLock
➣ ReentrantLock は再入可能ミューテックスロックで、「排他ロック」とも呼ばれます。
➣ ReentrantLock ロックは、同じ時点で 1 つのスレッド ロックによってのみ保持できます。再入可能とは、1 つのスレッドが複数回ロックを取得できることを意味します。
➣ ReentrantLock は「フェアロック」と「アンフェアロック」に分けられます。それらの違いは、ロック取得メカニズムの公平性と実行速度に反映されます。
➣ ReentrantLock は、FIFO 待機キューを介してロックを取得するすべてのスレッドを管理します。
ReentrantLock は排他ロックです. ロックを取得した後は, そのすべての操作はスレッドに排他的です. 他のスレッドはロックを取得する前に待機する必要があります.
public class ReentrantLock extends Object は Lock を実装し、Serializable
ReentrantLock は公正なロックと不公正なロックに分けられます。これら 2 つのロックの有効化も非常に簡単に制御できます
。 lock, NonfairSync): public ReentrantLock();
• 次のパラメータで構築: public ReentrantLock(Boolean fair);
①fair = true; 公平なロック、FairSync を意味;
②fair = false; 不公平なロック、NonfairSync; を意味します。

**例:** マルチスレッドの発券ハンドラーを定義する

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestDemo {
    
    
    public static void main(String[] args) throws Exception {
    
    
        Ticket ticket = new Ticket() ; // 多个线程要共享同一个数据资源
        for (int x = 0 ; x < 6 ; x ++) {
    
    
            new Thread(() -> {
    
    
                while (true) {
    
    
                    ticket.sale();     // 卖票处理
                }
            }).start();
        }
    }
}
class Ticket {
    
    
    private Lock myLock = new ReentrantLock();
    private int count = 100; // 一共10张票
    public void sale() {
    
     // 进行卖票处理
        myLock.lock(); // 进入到阻塞状态,一直到unlock执行后解除阻塞
        try {
    
    
            if (this.count > 0) {
    
    
                System.out.println(Thread.currentThread().getName()
                        + "卖票,ticket = " + this.count --);
            }
        } finally {
    
    
            myLock.unlock();   // 不管最终如何结果一定要进行解锁
        }
    }
}

現在のコードは、同期を直接使用するよりも簡単であり、ロック処理メカニズムはより直感的です. ロックに lock() を使用する場合、次の 2 つの状況が考慮されることがソース コードからわかります。
ここに画像の説明を挿入

公平なロック処理を実行する場合、スレッド オブジェクトがロックされるたびに、「acquire(1)」メソッドを使用してそれを示します。ロックを解除する場合、「sync.release(1)」リリース メソッドが使用されます。1 は解放を​​意味します.

[読み書きロック: ReadWriteLock ]
いわゆる読み書きロックは、データが書き込まれるときの「書き込みロック」と、データが読み取られるときの「読み取りロック」の 2 つのロックを指します。読み取りロックは複数のオブジェクトで読み取ることができますが、安全な同期処理操作。
➣ 読み取りロックと書き込みロックに分割され、複数の読み取りロックは相互に排他的ではなく (共有ロック)、読み取りロックと書き込みロックは相互に排他的であり、jvm 自体によって制御され、開発者は対応するロックをインストールするだけで済みます; ➣ ReentrantReadWriteLock は 2 つのロックを使用し
ますこの問題を解決するために、読み取りロック (複数のスレッドが同時に読み取ることができる) と書き込みロック (単一スレッドの書き込み) が使用されます。
➣ ReadLock は複数のスレッドによって保持され、動作時に WriteLock を除外することができ、WriteLock は完全に排他的です. この機能は最も重要です. なぜなら, 読み取り頻度が高く書き込みが比較的少ないデータ構造の場合, この種のロック同期メカニズムを使用することができます.同時実行の量を増やします。
ここに画像の説明を挿入

以下に銀行預金プログラムを書きます. 現在, あなたの銀行口座に 10 人が預金しています. 預金には排他ロック (書き込みロック) を使用する必要があり, 読み取り時にはすべてのスレッドが読み取り可能であり, 共有ロック (読み取りロック) を使用する必要があります. .ロック)。ReadWriteLock インターフェイスでは、ロックを取得する次の 2 つのメソッドを見つけることができます:
書き込みロックを取得する: public Lock writeLock();
読み取りロックを取得する: public Lock readLock();
: 読み取り/書き込みロックの処理操作を実装する

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
public class TestDemo {
    public static void main(String[] args) throws Exception {
        Account account = new Account("迪丽热巴", 15.0);
        double money[] = new double[] { 5.0, 300.0, 5000.0, 50000.0, 1000.0 };
        for (int x = 0; x < 2; x++) { // 设置两个写线程
            new Thread(() -> {
                for (int y = 0; y < money.length; y++) {
                    account.saveMoney(money[y]);
                }
            }, "存款用户-" + x).start();
        }
        for (int x = 0; x < 10; x++) { // 设置十个读线程
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName()
                        + "、查账,账户名:" + account.getName() + "、资产总额:"
                        + account.loadMoney());
            },"收款人-" + x).start();
        }
    }
}
 
class Account {
    private String name; // 开户名
    private double asset = 10.0; // 银行资产
    // 读写分离
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();
    public Account(String name, double asset) {
        this.name = name;
        this.asset = asset;
    }
    // 进行存款处理
    public boolean saveMoney(double money) {
        this.rwLock.writeLock().lock(); // 对写入数据进行锁定处理
        String tname = Thread.currentThread().getName();
        try {
            System.out.println("【(" + tname + ")存款-BEFORE】存款金额:" + money);
            TimeUnit.SECONDS.sleep(1);
            if (money > 0.0) { // 如果要存款肯定是要有钱
                this.asset += money;
                return true; // 存款成功
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("【("+ tname + ")存款-AFTER】总金额:" + this.asset);
            this.rwLock.writeLock().unlock(); // 进行解锁处理
        }
        return false;
    }
 
    public String getName() {  return this.name; }
 
    public double loadMoney() { // 返回当前的资金
        try {
            this.rwLock.readLock().lock();
            return this.asset;
        } finally { this.rwLock.readLock().unlock();  }
    }
}

ここに画像の説明を挿入

排他ロックの処理速度は遅いが、スレッドデータのセキュリティを保証できるのに対し、共有ロックの処理速度は高速であり、複数のスレッドに対するロック処理メカニズムであり、この読み取りと書き込みの処理関係は、重要なクラス セットである ConcurrentHashMap のコア実装です。

ロックの精密な制御:Condition
基本的なロックは以前にも触れたことがありますが、処理時のConditionというインターフェースもあり、このインターフェースはユーザーが作成してロックオブジェクトを作成することができます。
➣ 条件は、ロックをより正確に制御するために使用されます。Condition の await() メソッドは Object の wait() メソッドに相当し、Condition の signal() メソッドは Object の notify() メソッドに相当し、Condition の signalAll() メソッドは notifyAll( ) オブジェクトのメソッド。違いは、Object の wait()、notify()、および notifyAll() メソッドが「同期ロック」/「共有ロック」と組み合わせて使用​​されることです。
ここに画像の説明を挿入
ここに画像の説明を挿入

: 条件の基本的な使用法を観察する

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestDemo1 {
    
    
    public static String msg = null ;  // 设置一个字符串
    public static void main(String[] args) throws Exception {
    
    
        // 实例化Lock接口对象
        Lock myLock = new ReentrantLock();
        // 创建一个新的Condition接口对象
        Condition condition = myLock.newCondition();
        // 如果现在不进行锁定,那么Condition无法执行等待处理机制,会出现“IllegalMonitorStateException”
        myLock.lock(); // 现在是在主线程之中执行了一个lock()处理
        try {
    
    
            new Thread(()->{
    
    
                myLock.lock() ;
                try {
    
    
                    msg = "www.baidu.com" ;
                    condition.signal(); // 唤醒等待的Condition
                } finally {
    
    
                    myLock.unlock();
                }
            }) .start();
            condition.await(); // 线程等待
            System.out.println("****主线程执行完毕,msg = " +msg);
        } finally {
    
    
            myLock.unlock();   // 解除阻塞状态
        }
    }
}

以前のオブジェクトと比較すると、唯一の違いは、明示的な synchronized キーワードが現在表示されていないことと、lock インターフェースの locked() および unlock() メソッドの代わりに、ブロックされた状態 (同期された状態) で使用できることです。 ) Condition インターフェイスの await() と signal() は、待機とウェイクアップの操作を実行します。Condition の基本的な使用方法を明確にした後、少し単純で興味深いプログラムを実装してみましょう. 配列操作クラスでは、実際にはデータ バッファー操作として使用できる、より大きな役割があります。
ここに画像の説明を挿入

例: データ バッファー制御の実装

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class TestDemo2 {
    
    
    public static void main(String[] args) throws Exception {
    
    
        DataBuffer<String> buffer = new DataBuffer<String>() ;
        for (int x = 0 ; x < 3 ; x ++) {
    
       // 创建三个写线程
            new Thread(()->{
    
    
                for (int y = 0 ; y < 2 ; y ++) {
    
    
                    try {
    
    
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    buffer.put(Thread.currentThread().getName() + "写入数据,y = " + y);
                }
            },"生产者-" + x).start();
        }
        for (int x = 0 ; x < 5 ; x ++) {
    
       // 创建五个读线程
            new Thread(()->{
    
    
                while(true) {
    
    
                    try {
    
    
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println("【("+Thread.currentThread().getName()+")CONSUMER】" + buffer.get());
                }
            },"消费者-" + x).start();
        }
    }
}
 
// 进行数据的缓冲的操作控制,该缓冲可以保存各种数据类型
class DataBuffer<T> {
    
    
    // 该类之中保存的数组的长度个数为5
    private static final int MAX_LENGTH = 5 ;
    // 定义一个数组进行全部数据的保存控制
    private Object [] data = new Object [MAX_LENGTH] ;
    // 创建数据锁
    private Lock myLock = new ReentrantLock();
    // 数据保存的Condition控制
    private Condition putCondition = myLock.newCondition() ;
    // 数据取得的Condition控制
    private Condition getCondition = myLock.newCondition() ;   
    private int putIndex = 0 ; // 保存数据的索引
    private int getIndex = 0 ; // 读取数据的索引
    private int count = 0 ;    // 当前保存的元素个数
    public T get() {
    
               // 根据缓冲读取数据
        Object takeObject = null ;
        this.myLock.lock();
        try {
    
    
            if (this.count == 0) {
    
     // 没有写入
                this.getCondition.await(); // 读取的线程要进行等待
            }
            // 读取指定索引数据
            takeObject = this.data[this.getIndex ++] ; 
            if (this.getIndex == MAX_LENGTH) {
    
    
                this.getIndex = 0 ;    // 重新开始读
            }
            this.count -- ;// 因为读取了一个数据之后,现在需要减少个数
            this.putCondition.signal(); // 告诉写线程可以写入
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            this.myLock.unlock();
        }
        return (T) takeObject ;
    }
    // 进行缓冲数据的写入处理
    public void put(T t) {
    
    
        // 进入独占锁定状态
        this.myLock.lock(); 
        try {
    
    
            // 保存的数据量已经满了
            if (this.count == MAX_LENGTH) {
    
        
                // 暂时先别进行数据保存了
                this.putCondition.await(); 
            }
            // 保存当前的数据
            this.data[this.putIndex ++] = t ;  
            // 现在索引已经写满
            if (this.putIndex == MAX_LENGTH) {
    
     
                // 重置数组操作的索引脚标
                this.putIndex = 0 ;    
            }
            this.count ++ ;     // 保存的个数需要做一个追加
            this.getCondition.signal(); // 唤醒消费线程
            System.out.println("【(" + Thread.currentThread().getName() + ")写入缓冲-put()】" + t);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            this.myLock.unlock(); // 不管如何最终一定要进行解锁
        }
    }
}

ここに画像の説明を挿入

プロデューサー モデルとコンシューマー モデルの実装については、マルチスレッドの基本的な実装モデルを説明するだけでなく、上記のモデルを使用して Lock と Condition を使用してロックを正確に制御することもできます。

[ブロッキングプリミティブ: LockSupport ]
java.util.concurrent.locks.LockSupport は独立したクラスです. このクラスの主な機能は suspend() (スレッドの中断) と resume()(実行への応答) メソッドを解決することです.メソッドは本質的にデッドロックの疑いがあるため、JDK1.4以降は推奨されないメソッドとして挙げられていましたが、JDKがJUCアーキテクチャを開発した後、JUCを考慮したアーキテクチャのさまざまな実装メカニズムが復元を試み始めました。中断:
public static void park(オブジェクト ブロッカー); 復元:
public static void unpark(スレッド スレッド);
例: 中断を観察して実行を再開する

import java.util.concurrent.locks.LockSupport;
public class TestDemo3 {
    
    
    public static String msg = null ;  // 设置一个字符串
    public static void main(String[] args) throws Exception {
    
    
        // 获得当前的线程操作类
        Thread mainThread = Thread.currentThread();
        new Thread(()->{
    
    
            try {
    
    
                msg = "www.baidu.com" ;
            } finally {
    
      // 解锁关起状态
                LockSupport.unpark(mainThread);
            }
        }) .start();
        LockSupport.park(mainThread);
        System.out.println("********** 主线程执行完毕,msg="+msg);
    }
}

これらの処理方法は、実際には元のスレッド モデルの実装メカニズムに最適です。これらのクラスを使用する利点は、スレッド同期ロックを簡単かつ簡単に実装できることと、デッドロックによって引き起こされる問題を回避できることです。

おすすめ

転載: blog.csdn.net/kongfanyu/article/details/109838479