Javaコンカレントプログラミング-スピンロックの深い理解

1.スピンロックとは

スピンロック:スレッドがロックを取得しているときを指します。ロックが他のスレッドによって取得されている場合、スレッドはループで待機し、ロックが取得されるまで、ロックが正常に取得できるかどうかを常に判断します。ループを終了します。
ロックを取得するスレッドはアクティブですが、効果的なタスクを実行していません。このロックを使用すると、ビジー待機が発生します。

2. Javaはどのようにスピンロックを実装しますか?

スピンロックの実装例を見てみましょう。java.util.concurrentパッケージは、同時プログラミング用の多くのクラスを提供します。これらのクラスを使用すると、マルチコアCPUマシンでのパフォーマンスが向上します。主な理由は、これらのクラスのほとんどが使用されるためです。 (失敗再試行モード)同期モードでの悲観的ロックの代わりに楽観的ロック。


class spinlock {
    private AtomicReference<Thread> cas;
    spinlock(AtomicReference<Thread> cas){
        this.cas = cas;
    }
    public void lock() {
        Thread current = Thread.currentThread();
        // 利用CAS
        while (!cas.compareAndSet(null, current)) { //为什么预期是null??
            // DO nothing
            System.out.println("I am spinning");
        }
    }

    public void unlock() {
        Thread current = Thread.currentThread();
        cas.compareAndSet(current, null);
    }
}

lock()メソッドで使用されるCAS。最初のスレッドAがロックを取得すると、whileループに入らずに正常に取得できます。この時点でスレッドAがロックを解除しないと、別のスレッドBが再度ロックを取得します。 CASが満たされていないため、whileループに入り、Aスレッドがロック解除メソッドを呼び出してロックを解除するまで、CASが満たされているかどうかを判断し続けます。

スピンロック検証コード

package ddx.多线程;

import java.util.concurrent.atomic.AtomicReference;

public class 自旋锁 {
    public static void main(String[] args) {
        AtomicReference<Thread> cas = new AtomicReference<Thread>();
        Thread thread1 = new Thread(new Task(cas));
        Thread thread2 = new Thread(new Task(cas));
        thread1.start();
        thread2.start();
    }

}

//自旋锁验证
class Task implements Runnable {
    private AtomicReference<Thread> cas;
    private spinlock slock ;

    public Task(AtomicReference<Thread> cas) {
        this.cas = cas;
        this.slock = new spinlock(cas);
    }

    @Override
    public void run() {
        slock.lock(); //上锁
        for (int i = 0; i < 10; i++) {
            //Thread.yield();
            System.out.println(i);
        }
        slock.unlock();
    }
}

前のAtomicReferenceクラスを介してスピンロックcasが作成され、次に2つのスレッドが作成され、別々に実行されました。結果は次のとおりです。


0
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
1
I am spin
I am spin
I am spin
I am spin
I am spin
2
3
4
5
6
7
8
9
I am spin
0
1
2
3
4
5
6
7
8
9

Javaコンカレントプログラミング-スピンロックの深い理解

出力結果の分析から、まず、lockメソッドの実行時にスレッドがロックを取得したと想定できることがわかります。

cas.compareAndSet(null、current)

参照をスレッド1参照に変更し、whileループをスキップして、印刷機能を実行します。

2番目のスレッドもこの時点でlockメソッドに入り、その期待値を見つけました!=値を更新してから、whileループに入り、出力します

私は回転しています。次の赤い文字から、Javaのスレッドは常にcpuタイムスライスを占有し、常に実行されているわけではなく、プリエンプティブスケジューリングを使用しているため、上記の2つのスレッドは実行現象を交互に繰り返すと結論付けることができます。

Javaスレッドの実現は、システムの軽量スレッドにマップされます。軽量スレッドには、システムの対応するカーネルスレッドがあります。カーネルスレッドのスケジューリングは、システムスケジューラによってスケジュールされるため、Javaのスレッドスケジューリング方法は、システムのカーネルスケジューリングに依存します。主流のオペレーティングシステムのスレッド実装が先制的であるのはたまたまです。

3.スピンロックの問題

スピンロックを使用すると、次の問題が発生します
。1。スレッドがロックを長時間保持すると、ロックの取得を待機している他のスレッドがループ待機状態に入り、CPUを消費します。不適切な使用は、非常に高いCPU使用率を引き起こします。
2.上記のJavaによって実装されたスピンロックは公平ではありません。つまり、最初にロックを取得するために最も長い待機時間でスレッドを満足させることはできません。不当なロックには「スレッド不足」の問題があります。

4.スピンロックの利点

  1. スピンロックによってスレッドの状態が切り替わることはなく、常にユーザー状態になります。つまり、スレッドは常にアクティブになります。スレッドがブロッキング状態になることはなく、不要なコンテキストの切り替えが減り、実行速度が速くなります。
  2. 非スピンロックは、ロックが取得されていないときにブロッキング状態になり、カーネル状態になります。ロックが取得されると、カーネル状態から回復する必要があり、スレッドコンテキストスイッチが必要になります。(スレッドがブロックされた後、カーネル(Linux)スケジューリング状態になります。これにより、システムがユーザーモードとカーネルモードの間を行き来し、ロックのパフォーマンスに深刻な影響を及ぼします)

5.リエントリースピンロックと非リエントリースピンロック

記事の冒頭のコードを注意深く分析すると、再入力はサポートされていないことがわかります。つまり、スレッドが初めてロックを取得したとき、ロックが解放される前に再度ロックを取得します。 2回目は正常に取得できません。CASが満たされていないため、2回目の取得はwhileループに入り、待機します。再入力ロックの場合、2回目の取得は成功するはずです。
また、2回目の取得に成功したとしても、1回目のロック解除時に2回目の取得したロックが解除されるので無理です。

たとえば、コードを次のように変更します。

@Override
    public void run() {
        slock.lock(); //上锁
        slock.lock(); //再次获取自己的锁!由于不可重入,则会陷入循环
        for (int i = 0; i < 10; i++) {
            //Thread.yield();
            System.out.println(i);
        }
        slock.unlock();
    }

実行結果は無期限に出力され、無限のループに陥ります! 

リエントリーロックを実現するには、ロックを取得したスレッドの数を記録するカウンターを導入する必要があります。


public class ReentrantSpinLock {
    private AtomicReference<Thread> cas = new AtomicReference<Thread>();
    private int count;
    public void lock() {
        Thread current = Thread.currentThread();
        if (current == cas.get()) { // 如果当前线程已经获取到了锁,线程数增加一,然后返回
            count++;
            return;
        }
        // 如果没获取到锁,则通过CAS自旋
        while (!cas.compareAndSet(null, current)) {
            // DO nothing
        }
    }
    public void unlock() {
        Thread cur = Thread.currentThread();
        if (cur == cas.get()) {
            if (count > 0) {// 如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟
                count--;
            } else {// 如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。
                cas.compareAndSet(cur, null);
            }
        }
    }
}

同様に、lockメソッドは、最初に現在のスレッドがロックを取得したかどうかを判別し、次にカウントを1つ増やして再入力し、直接戻ります。ロック解除メソッドは、最初に現在のスレッドがロックを取得したかどうかを判別します。取得した場合は、最初にカウンターを判別し、1ずつデクリメントを続け、ロックを解除し続けます。

 リエントリースピンロックコードの検証


//可重入自旋锁验证
class Task1 implements Runnable{
    private AtomicReference<Thread> cas;
    private ReentrantSpinLock slock ;

    public Task1(AtomicReference<Thread> cas) {
        this.cas = cas;
        this.slock = new ReentrantSpinLock(cas);
    }

    @Override
    public void run() {
        slock.lock(); //上锁
        slock.lock(); //再次获取自己的锁!没问题!
        for (int i = 0; i < 10; i++) {
            //Thread.yield();
            System.out.println(i);
        }
        slock.unlock(); //释放一层,但此时count为1,不为零,导致另一个线程依然处于忙循环状态,所以加锁和解锁一定要对应上,避免出现另一个线程永远拿不到锁的情况
        slock.unlock();
    }
}

6.スピンロックとミューテックスロックの類似点と相違点

  • スピンロックと相互除外ロックはどちらも、リソース共有を保護するためのメカニズムです。
  • スピンロックであろうとミューテックスロックであろうと、いつでも最大で1つのホルダーを持つことができます。
  • ミューテックスロックを取得しているスレッドがすでに占有されている場合、スレッドはスリープ状態になります。スピンロックを取得しているスレッドはスリープしませんが、ループでロックが解放されるのを待ちます。

7.まとめ

  • スピンロック:スレッドがロックを取得するときに、ロックが別のスレッドによって保持されている場合、現在のスレッドはロックが取得されるまでループで待機します。
  • スピンロック待機期間中、スレッドの状態は変化せず、スレッドは常にユーザーモードでアクティブになります。
  • スピンロックがロックを長時間保持すると、ロックの取得を待機している他のスレッドがCPUを使い果たしてしまいます。
  • スピンロック自体は公平性を保証することも、再入国を保証することもできません。
  • スピンロックに基づいて、公正でリエントリーロックを実現できます

終わり

これでこの記事は終わりです。最後の友達に会ってくれてありがとう。最後にみんなが見えます。出発する前に親指を立ててください。何か問題があれば訂正してください。

おすすめ

転載: blog.51cto.com/14969174/2542996