ホームページ  >  記事  >  Java  >  Java でのスレッド セーフ実装と CLH キュー原理の簡単な紹介 (コード例)

Java でのスレッド セーフ実装と CLH キュー原理の簡単な紹介 (コード例)

不言
不言オリジナル
2018-09-17 15:23:072004ブラウズ

この記事では、Java および CLH キューの原則についての簡単な紹介 (コード例) を紹介します。必要な方は参考にしてください。

同期のブロック

Java では、マルチスレッドの同時実行の問題を解決するために、相互排他的な同期を実現するために synchronized キーワードをよく使用します。共有データへのアクセス。 synchronzied キーワードがコンパイルされると、monitorenter とmonitorexit という 2 つのバイトコード命令が、synchronized に含まれる同期コード ブロックの前後に追加されます。 synchronized キーワードでは、ロックおよびロック解除するオブジェクトを指定する必要があります。例:

public class Main {

    private static final Object LOCK = new Object();
    
    public static void fun1() {
        synchronized (LOCK) {
            // do something
        }
    }
    
    public static void fun2() {
        synchronized (LOCK) {
            // do something
        }
    }
}

オブジェクトが明示的に指定されていない場合、同期された変更がインスタンス メソッドであるか静的メソッドであるかによって、オブジェクト インスタンスをオブジェクトとして使用するか、クラスのクラス インスタンスを使用するかが決まります。例:

public class SynchronizedTest {
    public synchronized void doSomething() {
        //采用实例对象作为锁对象
    }
}
public class SynchronizedTest {
    public static synchronized void doSomething() {
        //采用SynchronizedTest.class 实例作为锁对象
    }
}

同期された実装に基づいて相互排他をブロックしているため、オペレーティング スレッドをブロックするだけでなく、オペレーティング システム レベルでネイティブ スレッドをウェイクアップまたはブロックし、移行する必要もあります。ユーザー状態からカーネル状態への変換にはユーザー コードの実行時間よりも時間がかかる場合があるため、同期は Java 言語における「重量ロック」であるとよく言われます。

ノンブロッキング同期

楽観的ロックと悲観的ロック

synchronized キーワードを使用した同期この方法の主な問題は、スレッドのブロックとウェイクアップによって生じるパフォーマンスの消費です。同期のブロックは悲観的な同時実行戦略であり、競合の可能性がある限り、ロックを実行する必要があると考えられます。
ただし、同期戦略には別の楽観的な戦略があります。楽観的な同時実行戦略では、他のスレッドもデータを操作したことが検出されない場合、操作は成功したとみなされます。他のスレッドもデータを操作する場合、通常、この楽観的ロック戦略はスレッドのブロックを必要とせず、ノンブロッキング同期の方法です。

CAS

オプティミスティック同時実行戦略には主に 2 つの重要な段階があります。1 つはデータの操作であり、もう 1 つは競合の検出、つまり、競合しているかどうかを検出することです。他のスレッドもこのデータに対して操作を実行していません。ここでのデータ操作と競合検出は atomic である必要があり、そうしないと i と同様の問題が発生しやすくなります。
CAS は比較とスワップを意味します。現在、ほとんどの CPU は CAS アトミック命令をネイティブにサポートしています。たとえば、IA64 および x86 の命令セットには、CAS 機能を完了するための cmpxchg などの命令があります。ハードウェアレベル。
CAS 命令には通常、値のメモリ アドレス、予期される古い値、および新しい値の 3 つのパラメータが必要です。 CAS 命令が実行されるとき、メモリ アドレスの値が予期された古い値と一致する場合、プロセッサはメモリ アドレスの値を新しい値で更新します。そうでない場合は更新しません。この操作は CPU 内でアトミックであることが保証されています。
Java には CAS 関連の API が多数ありますが、最も一般的なものは、AtomicInteger、## など、java.util.concurrent パッケージにあるさまざまなアトミック クラスです。 #AtomicReferenceなど。 これらのクラスはすべて CAS 操作をサポートしており、実際には
sun.misc.Unsafe の CompareAndSwapInt() メソッドと CompareAndSwapLong() メソッドに内部的に依存しています。 CAS は完全ではありませんが、原子性は保証できますが、有名な
ABA 問題があります。変数の値は、最初に読み取られたときは A であり、再度読み取られたときも A です。では、この変数は 2 回の読み取りの間で変化していないことを説明できますか?できません。この期間中に、変数は A から B に変化し、次に B から A に変化する可能性があります。2 回目の読み取り時には A と表示されますが、実際には変数が変化しています。コード ロジックによれば同時実行性のセキュリティに影響を与えないため、一般的なコード ロジックではこの ABA 問題は考慮されませんが、気になる場合は、CAS の代わりにブロッキング同期の使用を検討することもできます。実際、JDK 自体もこの ABA 問題の解決策を提供しており、ABA 問題を解決するために変数にバージョンを追加する AtomicStampedReference クラスを提供しています。

スピン ロック

synchronized で表される同期のブロック。ブロックされたスレッドはスレッド操作を再開するため、オペレーティング システムでのユーザー状態とカーネル状態が関与する必要があります。レベル間の切り替えは、システムのパフォーマンスに大きな影響を与えます。スピン ロックの戦略は、スレッドがロックを取得したときに、そのロックがすでに別のスレッドによって占有されていることが判明した場合、すぐに CPU の実行タイム スライスを放棄せず、「無意味な」ループに入り、スレッドのロックが放棄されたかどうかを確認します。

ただし、スピン ロックは
クリティカル セクション に適しています。ロックが長時間保持されると、スピン操作自体がシステムのパフォーマンスを無駄にします。

以下は単純なスピン ロックの実装です:

import java.util.concurrent.atomic.AtomicReference;
public class SpinLock {
   private AtomicReference<Thread> owner = new AtomicReference<Thread>();
   public void lock() {
       Thread currentThread = Thread.currentThread();
        // 如果锁未被占用,则设置当前线程为锁的拥有者
       while (!owner.compareAndSet(null, currentThread)) {}
   }

   public void unlock() {
       Thread currentThread = Thread.currentThread();
        // 只有锁的拥有者才能释放锁
       owner.compareAndSet(currentThread, null);
   }
}

上述的代码中, owner 变量保存获得了锁的线程。这里的自旋锁有一些缺点,第一个是没有保证公平性,等待获取锁的线程之间,无法按先后顺序分别获得锁;另一个,由于多个线程会去操作同一个变量 owner,在 CPU 的系统中,存在着各个 CPU 之间的缓存数据需要同步,保证一致性,这会带来性能问题。

公平的自旋

为了解决公平性问题,可以让每个锁拥有一个服务号,表示正在服务的线程,而每个线程尝试获取锁之前需要先获取一个排队号,然后不断轮询当前锁的服务号是否是自己的服务号,如果是,则表示获得了锁,否则就继续轮询。下面是一个简单的实现:

import java.util.concurrent.atomic.AtomicInteger;

public class TicketLock {
   private AtomicInteger serviceNum = new AtomicInteger(); // 服务号
   private AtomicInteger ticketNum = new AtomicInteger(); // 排队号

   public int lock() {
       // 首先原子性地获得一个排队号
       int myTicketNum = ticketNum.getAndIncrement();
       // 只要当前服务号不是自己的就不断轮询
       while (serviceNum.get() != myTicketNum) {
       }
       return myTicketNum;
    }

    public void unlock(int myTicket) {
        // 只有当前线程拥有者才能释放锁
        int next = myTicket + 1;
        serviceNum.compareAndSet(myTicket, next);
    }
}

虽然解决了公平性的问题,但依然存在前面说的多 CPU 缓存的同步问题,因为每个线程占用的 CPU 都在同时读写同一个变量 serviceNum,这会导致繁重的系统总线流量和内存操作次数,从而降低了系统整体的性能。

MCS 自旋锁

MCS 的名称来自其发明人的名字:John Mellor-Crummey和Michael Scott。
MCS 的实现是基于链表的,每个申请锁的线程都是链表上的一个节点,这些线程会一直轮询自己的本地变量,来知道它自己是否获得了锁。已经获得了锁的线程在释放锁的时候,负责通知其它线程,这样 CPU 之间缓存的同步操作就减少了很多,仅在线程通知另外一个线程的时候发生,降低了系统总线和内存的开销。实现如下所示:

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class MCSLock {
    public static class MCSNode {
        volatile MCSNode next;
        volatile boolean isWaiting = true; // 默认是在等待锁
    }
    volatile MCSNode queue;// 指向最后一个申请锁的MCSNode
    private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater
            .newUpdater(MCSLock.class, MCSNode.class, "queue");

    public void lock(MCSNode currentThread) {
        MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1
        if (predecessor != null) {
            predecessor.next = currentThread;// step 2
            while (currentThread.isWaiting) {// step 3
            }
        } else { // 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己已获得锁
            currentThread.isWaiting = false;
        }
    }

    public void unlock(MCSNode currentThread) {
        if (currentThread.isWaiting) {// 锁拥有者进行释放锁才有意义
            return;
        }

        if (currentThread.next == null) {// 检查是否有人排在自己后面
            if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4
                // compareAndSet返回true表示确实没有人排在自己后面
                return;
            } else {
                // 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
                // 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
                while (currentThread.next == null) { // step 5
                }
            }
        }
        currentThread.next.isWaiting = false;
        currentThread.next = null;// for GC
    }
}

MCS 的能够保证较高的效率,降低不必要的性能消耗,并且它是公平的自旋锁。

CLH 自旋锁

CLH 锁与 MCS 锁的原理大致相同,都是各个线程轮询各自关注的变量,来避免多个线程对同一个变量的轮询,从而从 CPU 缓存一致性的角度上减少了系统的消耗。
CLH 锁的名字也与他们的发明人的名字相关:Craig,Landin and Hagersten。
CLH 锁与 MCS 锁最大的不同是,MCS 轮询的是当前队列节点的变量,而 CLH 轮询的是当前节点的前驱节点的变量,来判断前一个线程是否释放了锁。
实现如下所示:

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class CLHLock {
    public static class CLHNode {
        private volatile boolean isWaiting = true; // 默认是在等待锁
    }
    private volatile CLHNode tail ;
    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
            . newUpdater(CLHLock.class, CLHNode .class , "tail" );
    public void lock(CLHNode currentThread) {
        CLHNode preNode = UPDATER.getAndSet( this, currentThread);
        if(preNode != null) {//已有线程占用了锁,进入自旋
            while(preNode.isWaiting ) {
            }
        }
    }

    public void unlock(CLHNode currentThread) {
        // 如果队列里只有当前线程,则释放对当前线程的引用(for GC)。
        if (!UPDATER .compareAndSet(this, currentThread, null)) {
            // 还有后续线程
            currentThread.isWaiting = false ;// 改变状态,让后续线程结束自旋
        }
    }
}

从上面可以看到,MCS 和 CLH 相比,CLH 的代码比 MCS 要少得多;CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋;CLH的队列是隐式的,通过轮询关注上一个节点的某个变量,隐式地形成了链式的关系,但CLHNode并不实际持有下一个节点,MCS的队列是物理存在的,而 CLH 的队列是逻辑上存在的;此外,CLH 锁释放时只需要改变自己的属性,MCS 锁释放则需要改变后继节点的属性。

CLH 队列是 J.U.C 中 AQS 框架实现的核心原理。

以上がJava でのスレッド セーフ実装と CLH キュー原理の簡単な紹介 (コード例)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。