Java同時プログラミングシリーズ---LockインタフェースとAQS初認識


一、javaのロック
1.1ロックとは
ロックは、複数のスレッドが共有リソースにアクセスすることを制御するために使用される方法であり、一般的に、1つのロックは、複数のスレッドが共有リソースに同時にアクセスすることを防止することができる(ただし、一部のロックでは、読み書きロックなどの複数のスレッドが同時に共有リソースにアクセスできる).ロックインタフェースが現れる前に、Javaプログラムはsynchronizedキーワードによってロック機能を実現したが、Java SE 5の後に、ロック機能を実現するためにロックインタフェース(および関連する実装クラス)が追加され、synchronizedキーワードと同様の同期機能を提供したが、使用時にロックを明示的に取得し、解放する必要がある.(synchronizedブロックまたはメソッドによって提供される)暗黙的に解放ロックを取得する利便性に欠けているが、ロック取得と解放の操作性、中断可能な取得ロック、タイムアウト取得ロックなど、複数のsynchronizedキーワードに備わっていない同期特性を有する.synchronizedキーワードを使用すると、暗黙的にロックが取得されますが、ロックの取得と解放が硬化します.つまり、取得してから解放されます.もちろん、この方式は同期の管理を簡素化しているが、拡張性は表示されていないロックの取得と解放が良い.例えば、1つのシーンでは、ハンドルがロック取得および解放され、まずロックAが取得され、その後ロックBが取得され、ロックBが取得された後、ロックAが同時にロックCを取得し、ロックCが取得された後、Bが同時にロックDを取得するなどしている.このような場面ではsynchronizedキーワードはそれほど簡単ではありませんが、ロックを使うのは簡単です.
1.2ロックの使用
ロックの使い方も簡単ですが、ロックの使い方は以下の通りです.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author : pengweiwei
 * @date : 2020/2/6 8:14   
 */
public class LockDemo {

    public static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        //  
        lock.lock();
        try {
            System.out.println("do something...");
        } finally {
            //   
            lock.unlock();
        }
    }


}

finallyブロックでは、ロックが取得された後、最終的に解放されることを保証するために、ロックが解放される.注意:ロックを取得するプロセスをtryブロックに書かないでください.ロック(カスタムロックの実装)を取得するときに異常が発生し、異常が放出されると同時に、ロックが理由もなく解放されるためです.
1.3 Lockインタフェースが提供するsynchronizedキーワードにはない主な特性
とくせい
説明
ロックをブロックせずに取得しようとする
現在のスレッドはロックを取得しようとしています.この-時刻ロックが他のスレッドに取得されていない場合、ロックは正常に取得され、保持されます.
ロックを中断して取得できる
synchronizedとは異なり、ロックを取得したスレッドは割り込みに応答し、ロックを取得したスレッドが割り込まれると割り込み異常が投げ出され、ロックが解放される
タイムアウト取得ロック
指定した締め切り時間までにロックを取得し、締め切り時間になってもロックを取得できない場合は
1.4 LockのAPI
方法
説明
void lock()
ロックを取得し、メソッドを呼び出すと現在のスレッドがロックを取得し、ロックが取得されるとメソッドから返されます.
void lockInterruptibly()
割り込み可能にロックを取得し、lock(メソッドの違いは、このメソッドが割り込み、すなわちロックの取得に応答することである.
boolean tryLock()
で、現在のスレッドがブロックされていない取得ロックを試行するのを中断し、メソッドを呼び出すとすぐに戻り、取得可能であればtrueを返し、そうでなければfalseを返すことができます.
boolean tryLock(longtime, TimeUnit unit)
タイムアウトの取得ロックは、現在のスレッドが次の3つの場合に返されます:1現在のスレッドがタイムアウト時間内にロックを取得した②現在のスレッドがタイムアウト時間内に中断された③タイムアウト時間終了
void unlock()
リリースロック
Condition newCondition()
待機通知コンポーネントを取得します.このコンポーネントは現在のロックにバインドされています.現在のスレッドはロックを取得してこそ、コンポーネントのwait()メソッドを呼び出すことができます.呼び出し後、現在のスレッドはロックを解放します.
二、キューシンクロナイザ(AQS)
2.1 AQSとは
キューシンクロナイザAbstractQueuedSynchronizerは、intメンバー変数(state)を使用してロックまたは他の同期コンポーネントを構築するための基礎フレームワークです.同期状態を表し、内蔵のFIFOキューを通してリソース取得スレッドのキュー作業を完了する.AQSの主な使用方法は継承であり、サブクラスは同期器を継承してその抽象的な方法を実現することによって同期状態を管理し、抽象的な方法の実現過程で同期状態を変更することは避けられない.この場合、AQSが提供する3つの方法(getState()、setState(int newState)とcompareAndSetState(int expect,int update))ステータスの変更が安全であることを保証するため、操作を行います.サブクラスは、カスタム同期コンポーネントの静的内部クラスとして定義されることを推奨します.同期器自体は同期インタフェースを実装していません.これは、カスタム同期コンポーネントの使用のためにいくつかの同期ステータスの取得と解放方法を定義しているだけです.同期器は、同期ステータスの独占的な取得をサポートすることもできます.同期状態の共有取得をサポートすることで、異なるタイプの同期コンポーネント(ReentrantLock、ReentrantReadWriteLock、CountDownLatchなど)を容易に実現できます.
2.2 AQSのインタフェースと例
シンクロナイザの設計は、テンプレートメソッドモードに基づいている.すなわち、使用者は、シンクロナイザを継承し、指定したメソッドを書き換える必要があり、その後、シンクロナイザをカスタム同期コンポーネントの実装に組み合わせ、シンクロナイザが提供するテンプレートメソッドを呼び出し、これらのテンプレートメソッドは、使用者が書き換えるメソッドを呼び出す.シンクロナイザが指定したメソッドを書き換える場合、シンクロナイザを使用する必要がある同期状態にアクセスまたは変更するには、次の3つの方法があります.
  • getState():現在の同期状態を取得します.
  • setState(int newState):現在の同期状態を設定します.
  • compareAndSetState(int expect,int update):CASを使用して現在の状態を設定します.この方法は状態設定の原子性を保証します.
  • 次の表は、AQSの書き換え方法です.
    メソッド名
    説明
    boolean tryAcquire(int arg)
    排他的に同期状態を取得します.この方法を実現するには、現在の状態を問合せ、同期状態が予想に合っているかどうかを判断してからCASで同期状態を設定する必要があります.
    boolean tryRelease(int arg)
    同期状態を排他的に解放し、同期状態を取得するスレッドを待つと同期状態を取得できます.
    int tryAcquireShared(int arg)
    共有式は同期状態を取得し、0以上の値を返し、取得に成功したことを示し、逆に取得に失敗したことを示す.
    boolean tryReleaseShared(int arg)
    共有型同期状態の解放
    boolean isHeldExclusively()
    現在の同期器が排他モードでスレッドによって占有されているかどうか、一般的にこの方法は現在のスレッドによって排他されているかどうかを示します.
    カスタム同期コンポーネントが実装されると、AQSが提供するテンプレートメソッドが呼び出されます.次のようになります.
    メソッド名
    説明
    void acquire(int arg)
    排他的に同期状態を取得し、現在のスレッドが同期状態を取得することに成功した場合、メソッドが返されます.そうしないと、同期キュー待機に入り、書き換えtryAcquire(int arg)メソッドが呼び出されます.
    void acquireInterruptibly(int arg)
    acquire(intarg)と同じですが、このメソッドは割り込みに応答し、現在のスレッドが同期状態に取得されずに同期キューに入り、現在のスレッドが割り込まれた場合、InterruptedExceptionを投げ出して戻ります.
    boolean tryAcquireNanos(int arg,long nanos)
    acquirelnterruptibly(int arg)に基づいてタイムアウト制限が追加され、現在のスレッドがタイムアウト時間内に同期状態を取得していない場合はfalseが返され、取得したらtrueが返されます.
    void acquireShared(int arg)
    共有型の取得同期状態は、現在のスレッドが同期状態を取得していない場合、エンクロージャ同期キューが待機します.独占型の取得との主な違いは、同じ時点で複数のスレッドが同期状態を取得できることです.
    void acquireSharedInterruptibly(int arg)
    この方法はacquireShared(int arg)と同様に応答が中断する
    boolean tryAcquireSharedNanos(int arg, long nanos)
    acquireSharedInterruptibly(int arg)に基づいてタイムアウト制限を追加
    boolean release(int arg)
    同期状態を解放した後、同期キュー内のノードに含まれるスレッドを起動する排他的な同期状態の解放
    boolean releaseShared(int arg)
    共有型の解放同期状態
    Collection getQueuedThreads()
    同期キューに待機しているスレッドのセットを取得
    2.3 AQSに基づいて独占ロックを実現する
    package com.example.demo.thread;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.AbstractQueuedSynchronizer;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    
    /**
     * @author : pengweiwei
     * @date : 2020/2/9 6:51   
     */
    public class Mutex implements Lock {
    
        //      ,      
        private static class Sync extends AbstractQueuedSynchronizer {
            //         
            protected boolean isHeldExclusively() {
                return getState() == 1;
            }
    
            //     0      
            public boolean tryAcquire(int acquires) {
                //      0   1            
                if (compareAndSetState(0, 1)) {
                    //             
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
                return false;
            }
    
            //    ,      0
            protected boolean tryRelease(int releases) {
                if (getState() == 0) throw new IllegalMonitorStateException();
                setExclusiveOwnerThread(null);
                setState(0);
                return true;
            }
    
            //     Condition,  condition      condition  
            Condition newCondition() {
                return new ConditionObject();
            }
        }
    
        private final Sync sync = new Sync();
    
        @Override
        public void lock() {
            sync.acquire(1);
        }
    
        @Override
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }
    
        @Override
        public boolean tryLock() {
            return sync.tryAcquire(1);
        }
    
        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return sync.tryAcquireNanos(1,unit.toNanos(time));
        }
    
        @Override
        public void unlock() {
            sync.release(1);
        }
    
        @Override
        public Condition newCondition() {
            return sync.newCondition();
        }
    }
    
    

    上記の例では、排他的ロックMutexは、同じ時点で1つのスレッドの排他的ロックのみを許可するカスタム同期コンポーネントです.Mutexでは、同期器を継承し、排他的な同期状態の取得と解放を実現する静的内部クラスが定義されています.tryAcquire(int acquires)メソッドでは、CAS設定に成功した場合(同期状態を1に設定)は、同期状態が取得されたことを表し、tryRelease(int releases)メソッドでは同期状態を0にリセットするだけです.
    テスト:
    package com.example.demo.thread;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * @author : pengweiwei
     * @date : 2020/2/9 7:42   
     */
    public class ThreadDemo3 {
    
        private Mutex mutex = new Mutex();
        private int i = 0;
    
        public void add() {
        	//  
            mutex.lock(); 
            try {
                i++;
            }finally {
            	//   
                mutex.unlock();
            }
    
        }
    
        public int res() {
            return i;
        }
    
        public static void main(String[] args) throws InterruptedException {
            int threadSize = 1000;
            ThreadDemo3 t = new ThreadDemo3();
            CountDownLatch countDownLatch = new CountDownLatch(threadSize);
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < threadSize; i++) {
                executorService.execute(() -> {
                            t.add();
                        }
                );
                countDownLatch.countDown();
            }
            countDownLatch.await();
            executorService.shutdown();
            System.out.println(t.res());
        }
    
    }
    
    

    自分で実装したロックを付けないと、データが一致しない場合があります.