マルチスレッドロック syn\lock の使用方法の詳細な説明

目次

一:synchronized

1.1: 同期は重量級のロックです

1.2: Synchronized の基礎となる実装原則

1.3:同期ロックの保管場所

1.4: 同期ロックのアップグレードプロセス

2: ロック

2.1ロック

2.2リエントラントロック

2.2.1 フェアロック/アンフェアロック

2.2.2 タイムアウトメカニズム

2.2.3 リエントラントロック

2.3 読み書きロック ReentrantReadWriteLock

3: 追加の補足:

3.1 ロックの詳細な分析: 読み取り/書き込みロックの原理


序文:

複数のスレッドが同じグローバル変数または静的変数を共有し、複数のスレッドがデータのセキュリティ上の問題なく同時にデータを読み取ることができますが、1 つのスレッドがデータを書き込むときに、他のスレッドが共有データを読み書きできる可能性があり、スレッドの安全性の問題が発生しました。

たとえば、特定のモジュールを手動でカプセル化する主キー ID インターフェイスがある場合、マルチスレッドの場合、グローバル変数がロックされず、主キーが同じになる状況が発生する可能性があります。

通常の操作モードでは、パブリック コードやグローバル変数のレプリケーションなどのパブリック リソースをロックします。次に例を示します。

1. シンクロロック(偏りロック、軽量ロック、重量ロック)

2. 揮発性ロックはスレッド間の可視性のみを保証できますが、データのアトミック性は保証できません。

3. jdk1.5 並行パッケージで提供される Atomic アトミック クラス

4.ロックロック

一:synchronized

例如:某一个代码块进行加锁处理:

       Object o=new Object();
        synchronized(o.getClass()){
            System.out.println(name+"开始执行");
            try {
                Thread.sleep(2000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }

            System.out.println(name+"执行完毕");
        }

例如:整理api进行加锁
    public synchronized String getValue(String name){
        System.out.println(name+"开始执行");
        try {
            Thread.sleep(2000);
        }catch (Exception e){
        }
        System.out.println(name+"执行完毕");
        retuen null;
    }
   

注: 同じオブジェクトがロックされている場合、アクセス リソースの競合が発生することに注意してください。同じオブジェクトでない場合は、これは当てはまりません。

SynchronizedTest t=new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.getValue("线程1");
            }
        }).start();
        SynchronizedTest t1=new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t1.getValue("线程2");
            }
        }).start();

SynchronizedTest t=new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.getValue("线程1");
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.getValue("线程2");
            }
        }).start();

 

 これがロック API のコード全体です。テストの状況は次のとおりです。ロック キーワードが静的メソッドまたは上記の最初のロックの例に配置されている場合、コードの一部のみがロックされます。このとき、ロックされているかどうかは関係ありません。オブジェクトかどうかに関係なく、リソースはロックされます。

1.1: 同期は重量級のロックです

同期は、オブジェクト内のモニター ロック (モニター) を通じて実装され、モニター ロックの本質は、基礎となるオペレーティング システムの Mutex ロック (相互排他ロック) に依存することによって実装されます。スレッド間を切り替えるには、オペレーティング システムがユーザー モードからコア モードに切り替える必要があり、このコストが非常に高く、状態間の遷移に比較的長い時間がかかるため、Synchronized は非効率的です。したがって、オペレーティング システム Mutex Lock によって実装されるこの種のロックを「重量ロック」と呼びます。

1.2: Synchronized の基礎となる実装原則

同期メソッドは、ACC_SYNCHRONIZED キーワードを通じて暗黙的にメソッドをロックします。スレッドによって実行されるメソッドが ACC_SYNCHRONIZED でマークされている場合、メソッドを実行する前にロックを取得する必要があります。
同期コード ブロックは、monitorenter およびmonitorexit の実行を通じてロックされます。スレッドがmonitorenterを実行する場合、後続のメソッドを実行する前にロックを取得する必要があります。スレッドが終了を監視するために実行されるときは、ロックを解放する必要があります。各オブジェクトはロック回数のカウンタを保持しており、カウンタが0以外の場合、ロックを取得したスレッドのみが再度ロックを取得できます。
 

1.3:同期ロックの保管場所

Synchronized で使用されるロックは Java のオブジェクト ヘッダーに存在します。新しいオブジェクトが生成された後、オブジェクトはメモリ内で主に 4 つの部分に分割されます。

Mark Word: オブジェクトのハッシュコード、GC 情報、ロック情報を 3 つの部分に分けて保存します。この部分は 8 バイトを占めます。

クラスポインタ:クラスオブジェクト情報へのポインタが格納される。64 ビット JVM では、圧縮ポインター オプション -ClassPointer ポインター: -XX:+UseCompressedClassPointers が 4 バイトで、8 バイトにはオンになりません。デフォルトでは有効になっています。

インスタンスデータ(インスタンスデータ):オブジェクト内の変数データを記録します。参照タイプ: -XX:+UseCompressedOops は 4 バイトであり、8 バイトまでは開かれません おっと通常のオブジェクト ポインター

パディング: アライメントに使用されます。64 ビット サービス版のオブジェクトでは、オブジェクトのメモリが 8 バイトで割り切れる必要があると規定されています。割り切れない場合、長期間アライメントに依存することはできません。 。例: new でオブジェクトを作成すると、メモリは 18 バイトしか占有しませんが、8 で割り切れると規定されているため、
padding=6 Mark Word の記憶構造は次のようになります。

1.4: 同期ロックのアップグレードプロセス

ロックの取得と解放によるパフォーマンスの消費を削減するために、Java SE 1.6 では「バイアスされたロック」と「軽量ロック」が導入されています。ロックには 4 つの状態があり、レベルの順序は低レベルから高レベルの順です。 、偏ったロック状態、軽量ロック状態、および重量ロック状態。ロックはアップグレードできますが、ダウングレードはできません。

1. 偏ったロック: ほとんどの場合、ロックはマルチスレッドの競合がないだけでなく、常に同じスレッドによって複数回取得されるため、スレッドがより低コストでロックを取得できるようにするために、偏ったロックが使用されます。が紹介されています。スレッドが同期ブロックにアクセスしてロックを取得すると、ロックバイアスを格納したスレッドIDをオブジェクトヘッダーとスタックフレームに記録し、その後、スレッドが同期ブロックに入ったときに、まずマークワードが指定しているかどうかを判断します。現在のスレッドはオブジェクト ヘッダーに保存され、バイアス ロックが存在する場合は、ロックを直接取得します。

2. 軽量ロック: 他のスレッドが偏ったロックをめぐって競合しようとすると、ロックは軽量ロックにアップグレードされます。スレッドが同期ブロックを実行する前に、JVM はまず現在のスレッドのスタック フレームにロック レコードを格納するためのスペースを作成し、オブジェクト ヘッダーの MarkWord をロック レコードへのポインタに置き換えます。成功すると、現在のスレッドがロックを取得します。失敗すると、他のスレッドがロックを獲得しようと競合していると判断され、現在のスレッドはスピンを使用してロックを取得しようとします。

3. 重量ロック: ロックが所定の位置で待機していると、CPU リソースが消費されます。したがって、スピンには特定の条件付き制御が必要です。そうでないと、スレッドが同期コード ブロックを長時間実行すると、ロックを待機しているスレッドがループし続け、代わりに CPU リソースを消費します。デフォルトでは、ロック スピンの回数は 10 回ですが、-XX:PreBlockSpin パラメーターを使用して、スピン ロックが待機する回数を設定できます。10 回経ってもロックが取得されなかった場合、ヘビーウェイト ロックにアップグレードされます。
 

2: ロック

序文:

同期と比較して、ロックはより柔軟にコードフラグメントをロックしようとします

例: 手動でロックを取得および解放する、最初にロック A を取得し、次にロック B を取得する、ロック B を取得した後にロック A を解放し、同時にロック C を取得するなど。 コード ロジックの制約


2.1ロック

Lock は 2 つの実装クラスがあり、特定の実装を区別できるインターフェイス クラスです。

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

2.2リエントラントロック

デモの例は次のとおりです。例外が発生してロックを解放できない場合は、tra を追加して例外をキャッチし、最後にロックを解放します。


    Lock lock = new ReentrantLock();
    public void run(){
        //获取锁 
        lock.lock();
        try {
           //公关资源处理,
        }catch (Exception e){

        }finally {
            //释放锁
            lock.unlock();
        }
       
    }


    

ReentrantLock 主にCAS+AQSキューを利用して実現されます。公平なロックと不公平なロックがサポートされており、どちらも同様に実装されます。

CAS は主に比較と置換に使用され、AQS は主にキューのロックに使用されます。ここに追加する時間があります。改善すべき TODO

2.2.1 フェアロック/アンフェアロック

デフォルトでは、機能しないロックが作成されます。公平なロックを作成するには、オブジェクトの作成時に true を追加します。

  • 不公平なロック: 別のスレッドが入ってきて同時にロックを取得しようとすると、このスレッドが最初にロックを取得する可能性があります。

  • 公平なロック: 別のスレッドが同時に取得を試みた場合、そのスレッドがキューの先頭にないことが判明すると、そのスレッドはキューの最後にキューに入れられ、そのスレッドがキューの先頭になります。キューがロックを取得します。

この構造は、 fair 公平なロックかどうかを指定するための受け渡しをサポートしています。

  • FairSync() フェアロック
  • NonfairSync() 不公平なロック

2.2.2 タイムアウトメカニズム

tryLock(long timeout, TimeUnit unit) 時間の経過とともにロックを取得する機能を提供します。そのセマンティクスは、指定された時間内にロックが取得された場合は戻りtrue、ロックが取得されなかった場合は戻りますfalse

2.2.3 リエントラントロック

1. リエントラントロック。リエントラント ロックとは、同じスレッドが同じロックを複数回取得できることを意味します。ReentrantLock と synchronized はどちらもリエントラント ロックです。

2. 中断可能なロック。割り込み可能なロックとは、ロックを取得しようとするプロセス中にスレッドが割り込みに応答できるかどうかを指します。Synchronized は割り込み不可能なロックであり、ReentrantLock は割り込み機能を提供します。

3. 公平なロックと不公平なロック。公平なロックとは、複数のスレッドが同時に同じロックを取得しようとしたときに、ロックが取得される順序はスレッドが到着した順であるのに対し、不公平なロックではスレッドが「キューにジャンプ」できることを意味します。 。Synchronized は不公平なロックであり、ReentrantLock のデフォルトの実装は不公平なロックですが、公平なロックとして設定することもできます
 

2.3 読み書きロック ReentrantReadWriteLock

デモの例:

百度copy一个demo代码,理解一下使用即可     
//声明读写锁
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //获取读锁
    private Lock readLock = readWriteLock.readLock();

    //获取写锁
    private Lock writeLock = readWriteLock.writeLock();

    private boolean isUpdate = true;

 public String read(String key) {
        readLock.lock();
        System.out.println(Thread.currentThread().getName() + " 读操作正在执行。。。");
        try {
            Thread.sleep(2000);
            return map.get(key);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
        }
        return null;
    }

    /**
     * 写操作
     * @param key
     * @param value
     */
    public void write(String key, String value) {
        writeLock.lock();
        System.out.println(Thread.currentThread().getName() + " 写操作正在执行。。。");
        try {
            Thread.sleep(2000);
            map.put(key, value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
        }
    }

    /**
     * 读写操作  锁的降级
     */
    public void readWrite(){
        readLock.lock();    //保证isUpdate是最新的数据
        if (isUpdate) {
            readLock.unlock();
            writeLock.lock();
            map.put("name", "admin");
            readLock.lock(); //锁的降级,如果此处不加读锁,直接执行下一步释放写锁,多线程情况下有可能又被写锁抢占资源,通过此方法实现将写锁降级为读锁
            writeLock.unlock();
        }

        String value = map.get("name");
        System.out.println(value);
        readLock.unlock();
    }

    public static void main(String[] args) throws Exception{
        ReadWriteLockTest readWriteLockTest = new ReadWriteLockTest();

        //写操作测试  可以知道写操作是互斥的
        Runnable runnable1 = () -> {
            for (int i = 0; i < 5; i++) {
                readWriteLockTest.write("key" + i, "value"+i);
            }

        };
        new Thread(runnable1).start();
        new Thread(runnable1).start();
        new Thread(runnable1).start();

        //读操作测试  可以知道读操作是可以并发执行的
        Runnable runnable2 = () -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(readWriteLockTest.read("key" + i));
            }

        };
        new Thread(runnable2).start();
        new Thread(runnable2).start();
        new Thread(runnable2).start();

        //读写操作测试 锁的降级
        Runnable runnable3 = () -> {
            for (int i = 0; i < 5; i++) {
                readWriteLockTest.readWrite();
            }

        };
        new Thread(runnable3).start();
        new Thread(runnable3).start();
        new Thread(runnable3).start();
    }
}

書き込み操作がない場合、複数のスレッドがリソースを同時に読み取ることに問題はないため、複数のスレッドが共有リソースを同時に読み取ることができるようにする必要がありますが、スレッドがこれらの共有リソースに書き込みたい場合は、他のスレッドがスレッドには共有リソースの読み取りを許可しないでください。読み取りおよび書き込み操作が実行されます。

このシナリオでは、JAVA の同時実行パッケージは読み取り/書き込みロック ReentrantReadWriteLock を提供します。これは 2 つのロックを表します。1 つは共有ロックと呼ばれる読み取り操作に関連するロックで、もう 1 つは排他ロックと呼ばれる書き込みに関連するロックで、次のように説明されます。続く

スレッドが読み取りロックに入るには次の前提条件があります。

  • 他のスレッドからの書き込みロックはありません。
  • 書き込み要求がないか、書き込み要求はありますが、呼び出しスレッドとロックを保持しているスレッドが同じです。

スレッドが書き込みロックに入る前提条件:

  • 他のスレッドからの読み取りロックはありません
  • 他のスレッドからの書き込みロックがない

つまり、次のようになります。

  • スレッドが読み取りロックを保持している場合、スレッドは書き込みロックを取得できません (書き込みロックを取得するときに、現在の読み取りロックが占有されていることが判明すると、読み取りロックがスレッドによって保持されているかどうかに関係なく、取得は直ちに失敗します)。現在のスレッド)。

  • スレッドが書き込みロックを保持している場合、スレッドは読み取りロックの取得を続行できます (読み取りロックの取得時に書き込みロックが占有されていることが判明した場合、書き込みロックが現在のスレッドによって占有されていない場合にのみ取得は失敗します)。 。

読み取り/書き込みロックには、次の 3 つの重要な特性があります。

(1) 公平な選択性: 不公平 (デフォルト) および公平なロック取得方法をサポートしており、スループットは依然として公平より優れています。

(2) 再エントリ: 読み取りロックと書き込みロックの両方がスレッドの再エントリをサポートします。

(3) ロックのダウングレード: 書き込みロックの取得、読み取りロックの取得、書き込みロックの解放の順序に従って、書き込みロックを読み取りロックにダウングレードできます。

3: 追加の補足:

3.1 ロックの詳細な分析: 読み取り/書き込みロックの原理

ブログ投稿を引用するには、リンクは次のとおりです。

読み取り/書き込みロックの読み取りロック原理: スレッドが読み取りロックを適用したい場合、最初に書き込みロックが保持されているかどうかを判断します。書き込みロックが保持されており、現在のスレッドが書き込みロックを保持しているスレッドではない場合、 - 1 が返されます。ロックの取得に失敗しました。待機キューに入って待機します。書き込みロックがスレッドによって保持されていない場合、または現在のスレッドと書き込みロックを保持しているスレッドが同じスレッドである場合、読み取りロックの取得が開始されます。スレッドは、最初に読み取りロックの数が 65535 を超えているかどうかを判断します。超えていない場合、CAS は状態変数の上位 16 ビットの値を変更します。つまり、状態の値は +1 です。このステップが失敗した場合は、この操作を成功するまでループします。CAS 変更が成功すると、読み取りロックが正常に取得されたことを意味します。現在のスレッドが最初の読み取りスレッドであるかどうかを判断します。そうであれば、最初のマルチスレッドと最初の読み取りカウンター (パフォーマンスと再入可能性のため) を設定します。初めての読み取りロック取得でない場合は、最初の読み取りスレッドと同じかどうかを判定し、最初の読み取りスレッドと同じスレッドであれば、初回読み取りカウンタを+1します。スレッドの読み取りが初めてではない場合、スレッドの読み取りが最後であるかどうかを判断し、最後である場合には、最終読み取りカウンタに 1 を加算します。そうでない場合は、新しいカウンターを作成し、最後の読み取りスレッドを独自のスレッドとして設定し、その読み取りカウンターを更新します。

protected final int11 tryAcquireShared(int unused) {
 
    Thread current = Thread.currentThread();
    int c = getState();
    // exclusiveCount(c) != 0 ---》 用 state & 65535 得到低 16 位的值。如果不是0,说明写锁别持有了。
    // getExclusiveOwnerThread() != current----> 不是当前线程
    // 如果写锁被霸占了,且持有线程不是当前线程,返回 false,加入队列。获取写锁失败。
    // 反之,如果持有写锁的是当前线程,就可以继续获取读锁了。
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
        // 获取锁失败
        return -1;
    // 如果写锁没有被霸占,则将高16位移到低16位。
    int r = sharedCount(c);// c >>> 16
    // !readerShouldBlock() 和写锁的逻辑一样(根据公平与否策略和队列是否含有等待节点)
    // 不能大于 65535,且 CAS 修改成功
    if (!readerShouldBlock() && r < 65535 && compareAndSetState(c, c + 65536)) {
        // 如果读锁是空闲的, 获取锁成功。
        if (r == 0) {
            // 将当前线程设置为第一个读锁线程
            firstReader = current;
            // 计数器为1
            firstReaderHoldCount = 1;
 
        }// 如果读锁不是空闲的,且第一个读线程是当前线程。获取锁成功。
         else if (firstReader == current) {// 
            // 将计数器加一
            firstReaderHoldCount++;
        } else {// 如果不是第一个线程,获取锁成功。
            // cachedHoldCounter 代表的是最后一个获取读锁的线程的计数器。
            HoldCounter rh = cachedHoldCounter;
            // 如果最后一个线程计数器是 null 或者不是当前线程,那么就新建一个 HoldCounter 对象
            if (rh == null || rh.tid != getThreadId(current))
                // 给当前线程新建一个 HoldCounter ------>详见下图get方法
                cachedHoldCounter = rh = readHolds.get();
            // 如果不是 null,且 count 是 0,就将上个线程的 HoldCounter 覆盖本地的。
            else if (rh.count == 0)
                readHolds.set(rh);
            // 对 count 加一
            rh.count++;
        }
        return 1;
    }
    // 死循环获取读锁。包含锁降级策略。
    return fullTryAcquireShared(current);
}

 

2 つの記事を引用:詳細な分析: AQS 原則

詳細な分析: 読み取り/書き込みロックの原理

おすすめ

転載: blog.csdn.net/qq_44691484/article/details/130186562