ホームページ Java &#&チュートリアル Java でのスレッド セーフ実装と CLH キュー原理の簡単な紹介 (コード例)

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

Sep 17, 2018 pm 03:23 PM
マルチスレッド化 同時

この記事では、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 までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

AI Hentai Generator

AI Hentai Generator

AIヘンタイを無料で生成します。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

C++ 関数の例外とマルチスレッド: 同時環境でのエラー処理 C++ 関数の例外とマルチスレッド: 同時環境でのエラー処理 May 04, 2024 pm 04:42 PM

C++ での関数例外処理は、マルチスレッド環境でスレッドの安全性とデータの整合性を確保するために特に重要です。 try-catch ステートメントを使用すると、特定の種類の例外が発生したときにそれをキャッチして処理し、プログラムのクラッシュやデータの破損を防ぐことができます。

Golang API 設計における同時実行性とコルーチンの適用 Golang API 設計における同時実行性とコルーチンの適用 May 07, 2024 pm 06:51 PM

同時実行性とコルーチンは、GoAPI 設計で次の目的で使用されます。 高パフォーマンス処理: 複数のリクエストを同時に処理してパフォーマンスを向上させます。非同期処理: コルーチンを使用してタスク (電子メールの送信など) を非同期に処理し、メインスレッドを解放します。ストリーム処理: コルーチンを使用して、データ ストリーム (データベース読み取りなど) を効率的に処理します。

PHPでマルチスレッドを実装するにはどうすればよいですか? PHPでマルチスレッドを実装するにはどうすればよいですか? May 06, 2024 pm 09:54 PM

PHP マルチスレッドとは、1 つのプロセスで複数のタスクを同時に実行することを指します。これは、独立して実行されるスレッドを作成することによって実現されます。 PHP の Pthreads 拡張機能を使用して、マルチスレッド動作をシミュレートできます。インストール後、Thread クラスを使用してスレッドを作成および開始できます。たとえば、大量のデータを処理する場合、データを複数のブロックに分割し、対応する数のスレッドを作成して同時処理することで効率を向上させることができます。

C++ のマルチスレッドで共有リソースを処理するにはどうすればよいですか? C++ のマルチスレッドで共有リソースを処理するにはどうすればよいですか? Jun 03, 2024 am 10:28 AM

ミューテックスは C++ でマルチスレッド共有リソースを処理するために使用されます。std::mutex を通じてミューテックスを作成します。 mtx.lock() を使用してミューテックスを取得し、共有リソースへの排他的アクセスを提供します。ミューテックスを解放するには mtx.unlock() を使用します。

マルチスレッド環境における C++ メモリ管理の課題と対策? マルチスレッド環境における C++ メモリ管理の課題と対策? Jun 05, 2024 pm 01:08 PM

マルチスレッド環境では、C++ メモリ管理はデータ競合、デッドロック、メモリ リークなどの課題に直面します。対策には次のものが含まれます: 1. ミューテックスやアトミック変数などの同期メカニズムの使用、 2. ロックフリーのデータ構造の使用、 4. (オプション) ガベージ コレクションの実装。

C++ でマルチスレッド プログラムをテストするための課題と戦略 C++ でマルチスレッド プログラムをテストするための課題と戦略 May 31, 2024 pm 06:34 PM

マルチスレッド プログラムのテストは、非再現性、同時実行エラー、デッドロック、可視性の欠如などの課題に直面しています。戦略には以下が含まれます。 単体テスト: 各スレッドの単体テストを作成して、スレッドの動作を検証します。マルチスレッド シミュレーション: シミュレーション フレームワークを使用して、スレッド スケジューリングを制御しながらプログラムをテストします。データ競合の検出: valgrind などのツールを使用して、潜在的なデータ競合を見つけます。デバッグ: デバッガー (gdb など) を使用して、ランタイム プログラムのステータスを調べ、データ競合の原因を見つけます。

Go 同時関数の単体テストのガイド Go 同時関数の単体テストのガイド May 03, 2024 am 10:54 AM

並行関数の単体テストは、同時環境での正しい動作を確認するのに役立つため、非常に重要です。同時実行機能をテストするときは、相互排他、同期、分離などの基本原則を考慮する必要があります。並行機能は、シミュレーション、競合状態のテスト、および結果の検証によって単体テストできます。

C++ マルチスレッド プログラミングにおけるデバッグとトラブルシューティングのテクニック C++ マルチスレッド プログラミングにおけるデバッグとトラブルシューティングのテクニック Jun 03, 2024 pm 01:35 PM

C++ マルチスレッド プログラミングのデバッグ手法には、データ競合アナライザーを使用して読み取りと書き込みの競合を検出し、同期メカニズム (ミューテックス ロックなど) を使用して競合を解決することが含まれます。スレッド デバッグ ツールを使用してデッドロックを検出し、ネストされたロックを回避し、デッドロック検出メカニズムを使用してデッドロックを解決します。データ競合アナライザーを使用してデータ競合を検出し、書き込み操作をクリティカル セクションに移動するか、アトミック操作を使用して解決します。パフォーマンス分析ツールを使用してコンテキストの切り替え頻度を測定し、スレッド数の削減、スレッド プールの使用、タスクのオフロードによって過剰なオーバーヘッドを解決します。

See all articles