ishowcode.eth

ishowcode.eth

区块链小白

Java マルチスレッドであなたの安全問題を解決する一篇

前言
プログラムが実行されると、プロセスに変換され、通常は複数のスレッドを含みます。通常、プロセス内の比較的時間のかかる操作(長いループ、ファイルのアップロードやダウンロード、ネットワークリソースの取得など)は、マルチスレッドを使用して解決されます。

例えば、日常生活の中で、銀行の引き出し問題や、複数の窓口での列車の切符の販売問題など、通常は同時実行の問題が関与し、そのためにマルチスレッド技術が必要です。

プロセス内に複数の同時実行スレッドが重要なデータのコードブロックに入ると、データを変更する過程でスレッドセーフの問題が発生する可能性が高く、データの異常を引き起こすことがあります。例えば、正常なロジックでは、同じ番号の切符は一度だけ販売されるべきですが、スレッドセーフの問題により複数回販売されてしまい、実際のビジネスに異常を引き起こすことがあります。

一般的に、私たちは「あるクラスはスレッドセーフである」「あるクラスはスレッドセーフでない」と言います。実際、スレッドセーフは「白か黒か」の単一選択問題ではありません。「スレッドセーフ」の安全性の程度を強から弱に並べると、Java 言語におけるさまざまな操作共有データを以下の 5 つのカテゴリに分けることができます:

  1. 不変
    Java 言語において、不変のオブジェクトは必ずスレッドセーフです。オブジェクトのメソッドの実装やメソッドの呼び出し元は、追加のスレッドセーフの保証措置を取る必要はありません。final キーワードで修飾されたデータは変更できず、信頼性が最も高いです。
    絶対スレッドセーフ
    絶対的なスレッドセーフは、Brian Goetz が示したスレッドセーフの定義を完全に満たします。この定義は非常に厳格であり、クラスが「実行環境に関係なく、呼び出し元が追加の同期措置を必要としない」状態に達するには、通常は大きなコストがかかります。

  2. 相対スレッドセーフ
    相対スレッドセーフは、通常私たちが「スレッドセーフ」と呼ぶクラスのことです。このクラスは、そのオブジェクトに対する単独の操作がスレッドセーフであることを保証する必要があります。呼び出す際に追加の保証措置を取る必要はありませんが、特定の順序での連続呼び出しには、呼び出し元で追加の同期手段を使用して呼び出しの正確性を保証する必要があるかもしれません。
    Java 言語では、大部分のスレッドセーフクラスは相対スレッドセーフに該当します。例えば、Vector、HashTable、Collections の synchronizedCollection () メソッドが保証するコレクションです。

  3. スレッド互換
    スレッド互換は、通常私たちが「スレッドセーフでない」と呼ぶクラスのことです。
    スレッド互換は、オブジェクト自体がスレッドセーフではないが、呼び出し元で正しく同期手段を使用することで、オブジェクトが同時実行環境で安全に使用できることを指します。Java API のほとんどのクラスはスレッド互換に該当します。前述の Vector や HashTable に対応するコレクションクラスの ArrayList や HashMap などです。

  4. スレッド対立
    スレッド対立は、呼び出し元が同期を取ったかどうかに関係なく、マルチスレッド環境で同時に使用できないコードを指します。Java 言語はもともとマルチスレッド特性を持っているため、スレッド対立のようなマルチスレッドを排除するコードは非常に少ないです。
    スレッド対立の例として、Thread クラスの suspend () および resume () メソッドがあります。2 つのスレッドが同時に 1 つのスレッドオブジェクトを保持し、一方がスレッドを中断しようとし、もう一方がスレッドを復元しようとする場合、同時に行われると、呼び出し時に同期が取られていても、対象スレッドはデッドロックのリスクがあります。このため、これらの 2 つのメソッドは廃止されました。
    ヒント:以下は本記事の本文内容であり、下記の例は参考用です。

第一種のスレッドセーフを実現する方法
 同期コードブロック

public class ThreadSynchronizedSecurity {  
    static int tickets = 15;  
    class SellTickets implements Runnable {  
        @Override  
        public void run() {  
            while (tickets > 0) {  
                // 同期コードブロック  
                synchronized (this) {  
                    if (tickets <= 0) {  
                        return;  
                    }  
                    System.out.println(Thread.currentThread().getName() + "--->售出第:  " + tickets + " 票");  
                    try {  
                        Thread.sleep(1);  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                    tickets--;  
                }  
                if (tickets <= 0) {  
                    System.out.println(Thread.currentThread().getName() + "--->售票结束!");  
                }  
            }  
        }  
    }  
    public static void main(String[] args) {  
        SellTickets sell = new ThreadSynchronizedSecurity().new SellTickets();  
        Thread thread1 = new Thread(sell, "1号窗口");  
        Thread thread2 = new Thread(sell, "2号窗口");  
        Thread thread3 = new Thread(sell, "3号窗口");  
        Thread thread4 = new Thread(sell, "4号窗口");  
        thread1.start();  
        thread2.start();  
        thread3.start();  
        thread4.start();  
    }  
}  

備考:

synchronized コードブロックを使用する際は、wait ()、notify ()、notifyAll () と組み合わせて使用することで、スレッド間の通信をさらに実現できます。wait () メソッドは占有しているオブジェクトロックを解放し、現在のスレッドは待機プールに入ります。CPU を解放し、他の待機中のスレッドがこのロックを奪うことができ、ロックを取得したスレッドがプログラムを実行できるようになります。スレッドの sleep () メソッドは、現在のスレッドが一定時間休眠することを示し、休眠中は CPU を一時的に解放しますが、オブジェクトロックは解放しません。つまり、休眠中は他のスレッドが同期保護されたコード内部に入ることはできず、現在のスレッドが休眠を終えると、再び CPU の実行権を取得し、同期保護されたコードを実行します。wait () と sleep () の最大の違いは、wait () はオブジェクトロックを解放し、sleep () はオブジェクトロックを解放しないことです。

notify () メソッドは、オブジェクトの wait () を呼び出して待機状態にあるスレッドを起こし、そのスレッドがオブジェクトロックを取得する機会を与えます。notify () を呼び出した後、現在のスレッドはすぐにロックを解放せず、現在のコードを実行し続け、synchronized 内のコードがすべて実行されるまでオブジェクトロックを解放しません。JVM は待機中のスレッドの中から 1 つのスレッドをスケジュールしてオブジェクトロックを取得し、コードを実行します。

wait () と notify () は必ず synchronized コードブロック内で呼び出す必要があります。notifyAll () はすべての待機中のスレッドを起こします。

次に、2 つのスレッドが交互に「A」と「B」を各 10 回印刷するプログラムを見てみましょう。以下の例のコードを参照してください:

package com.my.annotate.thread;  
public class ThreadDemo {  
    static final Object obj = new Object();  
    //第一个子线程  
    static class ThreadA implements Runnable {  
        @Override  
        public void run() {  
            int count = 5;  
            while (count > 0) {  
                synchronized (ThreadDemo.obj) {  
                    System.out.println("A-----" + count);  
                    count--;  
                    synchronized (ThreadDemo.obj) {  
                        //notify()メソッドは、オブジェクトのwait()を呼び出して待機状態にあるスレッドを起こし、そのスレッドがオブジェクトロックを取得する機会を与えます。  
                        //notify()を呼び出した後、現在のスレッドはすぐにロックを解放せず、現在のコードを実行し続け、synchronized内のコードがすべて実行されるまで、  
                        ThreadDemo.obj.notify();  
                        try {  
                            ThreadDemo.obj.wait();  
                        } catch (InterruptedException e) {  
                            // TODO Auto-generated catch block  
                            e.printStackTrace();  
                        }  
                    }  
                }  
            }  
        }  
    }  
    static class ThreadB implements Runnable {  
        @Override  
        public void run() {  
            int count = 5;  
            while (count > 0) {  
                synchronized (ThreadDemo.obj) {  
                    System.out.println("B-----" + count);  
                    count--;  
                    synchronized (ThreadDemo.obj) {  
                        //notify()メソッドは、オブジェクトのwait()を呼び出して待機状態にあるスレッドを起こし、そのスレッドがオブジェクトロックを取得する機会を与えます。  
                        //notify()を呼び出した後、現在のスレッドはすぐにロックを解放せず、現在のコードを実行し続け、synchronized内のコードがすべて実行されるまで、  
                        ThreadDemo.obj.notify();  
                        try {  
                            ThreadDemo.obj.wait();  
                        } catch (InterruptedException e) {  
                            // TODO Auto-generated catch block  
                            e.printStackTrace();  
                        }  
                    }  
                }  
            }  
        }  
    }  
    public static void main(String[] args) {  
        new Thread(new ThreadB()).start();  
        new Thread(new ThreadA()).start();  
    }  
}  

第二種のスレッドセーフを実現する方法
 同期メソッド

package com.my.annotate.thread;  
public class ThreadSynchroniazedMethodSecurity {  
    static int tickets = 15;  
    class SellTickets implements Runnable {  
        @Override  
        public void run() {  
            //同期メソッド  
            while (tickets > 0) {  
                synMethod();  
                try {  
                    Thread.sleep(1);  
                } catch (InterruptedException e) {  
                    // TODO Auto-generated catch block  
                    e.printStackTrace();  
                }  
                if (tickets <= 0) {  
                    System.out.println(Thread.currentThread().getName() + "--->售票结束");  
                }  
            }  
        }  
        synchronized void synMethod() {  
            synchronized (this) {  
                if (tickets <= 0) {  
                    return;  
                }  
                System.out.println(Thread.currentThread().getName() + "---->售出第 " + tickets + " 票 ");  
                tickets--;  
            }  
        }  
    }  
    public static void main(String[] args) {  
        SellTickets sell = new ThreadSynchroniazedMethodSecurity().new SellTickets();  
        Thread thread1 = new Thread(sell, "1号窗口");  
        Thread thread2 = new Thread(sell, "2号窗口");  
        Thread thread3 = new Thread(sell, "3号窗口");  
        Thread thread4 = new Thread(sell, "4号窗口");  
        thread1.start();  
        thread2.start();  
        thread3.start();  
        thread4.start();  
    }  
}  

第三種のスレッドセーフを実現する方法
Lock ロックメカニズム、Lock オブジェクトを作成し、lock () でロックし、unlock () でロックを解除して、指定されたコードブロックを保護します。

package com.my.annotate.thread;  
import java.util.concurrent.locks.Lock;  
import java.util.concurrent.locks.ReentrantLock;  
public class ThreadLockSecurity {  
    static int tickets = 15;  
    class SellTickets implements Runnable {  
        Lock lock = new ReentrantLock();  
        @Override  
        public void run() {  
            // Lockロックメカニズム  
            while (tickets > 0) {  
                try {  
                    lock.lock();  
                    if (tickets <= 0) {  
                        return;  
                    }  
                    System.out.println(Thread.currentThread().getName() + "--->售出第:  " + tickets + " 票");  
                    tickets--;  
                } catch (Exception e1) {  
                    // TODO Auto-generated catch block  
                    e1.printStackTrace();  
                } finally {  
                    lock.unlock();  
                    try {  
                        Thread.sleep(1);  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }  
            }  
            if (tickets <= 0) {  
                System.out.println(Thread.currentThread().getName() + "--->售票结束!");  
            }  
        }  
    }  
    public static void main(String[] args) {  
        SellTickets sell = new ThreadLockSecurity().new SellTickets();  
        Thread thread1 = new Thread(sell, "1号窗口");  
        Thread thread2 = new Thread(sell, "2号窗口");  
        Thread thread3 = new Thread(sell, "3号窗口");  
        Thread thread4 = new Thread(sell, "4号窗口");  
        thread1.start();  
        thread2.start();  
        thread3.start();  
        thread4.start();  
    }  
}  

synchronized は JVM レベルで実装されているため、システムはロックの解放を監視できます。一方、ReentrantLock はコードで実装されているため、システムは自動的にロックを解放できず、コード内の finally 句で明示的にロックを解放する必要があります。また、同時実行量が比較的小さい場合、synchronized は良い選択です。しかし、同時実行量が比較的高い場合、その性能低下は非常に深刻になるため、この場合 ReentrantLock は良い選択肢です。

二、スレッドセーフの実現方法
スレッドセーフを保証するために、同期手段が必要かどうかで分類し、同期方案と非同期方案に分けます。

  1. 互斥同期
    互斥同期は、最も一般的な並行性の正しさを保証する手段です。同期とは、マルチスレッドが共有データに同時にアクセスする際に、共有データが同時に 1 つのスレッドによってのみ使用されることを保証することを指します(同時に、1 つのスレッドだけが共有データを操作しています)。互斥は、同期を実現する手段の 1 つであり、クリティカルセクション、ミューテックス、セマフォは主な互斥実装方式です。したがって、これら 4 つの言葉の中で、互斥は原因であり、同期は結果です。互斥は手段であり、同期は目的です。

Java では、最も基本的な互斥同期手段は synchronized キーワードです。synchronized キーワードはコンパイル後、同期ブロックの前後に monitorenter と monitorexit という 2 つのバイトコードを生成します。これらのバイトコード命令は、ロックおよび解放するオブジェクトを指定するために、reference 型のパラメータを必要とします。

さらに、ReentrantLock も互斥を通じて同期を実現します。基本的な使い方では、ReentrantLock は synchronized と非常に似ており、同じスレッド再入特性を持っています。

互斥同期の主な問題は、スレッドのブロックとウェイクアップによって引き起こされる性能問題です。このため、この同期はブロッキング同期とも呼ばれます。問題の処理方法から見ると、互斥同期は悲観的な並行戦略の一種であり、正しい同期措置(例えばロックをかける)を行わなければ問題が発生するだろうと常に考えています。共有データが本当に競合するかどうかに関係なく、ロックをかける必要があります。

  1. 非ブロッキング同期
    ハードウェア命令セットの発展に伴い、競合検出に基づく楽観的並行戦略が登場しました。言い換えれば、最初に操作を行い、他のスレッドが共有データを争奪しなければ操作が成功します。共有データに競合が発生し、衝突が生じた場合は、他の補償措置を採用します(最も一般的な補償エラーは、成功するまで再試行を繰り返すことです)。このような楽観的並行戦略の多くの実装は、スレッドを一時停止する必要がないため、この同期操作は非ブロッキング同期と呼ばれます。

非ブロッキングの実装 CAS(compareandswap):CAS 命令は 3 つのオペランドを必要とし、それぞれメモリアドレス(Java では変数のメモリアドレスを理解し、V で表します)、古い期待値(A で表します)、新しい値(B で表します)です。CAS 命令が実行されると、CAS 命令は、V の値が古い期待値 A と一致する場合に限り、プロセッサが B を使用して V の値を更新します。そうでなければ、更新は実行されませんが、V の値が更新されたかどうかに関係なく、V の古い値が返されます。この処理プロセスは原子操作です。

CAS の欠点:

ABA 問題:CAS は値を操作する際に値が変化していないかを確認する必要があります。変化がなければ更新しますが、元の値が A で B に変わり、再び A に戻ると、CAS で確認すると値が変わっていないことがわかりますが、実際には変化しています。

ABA 問題の解決策は、バージョン番号を使用することです。変数の前にバージョン番号を追加し、変数が更新されるたびにバージョン番号を 1 つ増やします。これにより、A-B-A は 1A-2B-3C になります。JDK の atomic パッケージには、ABA 問題を解決するための AtomicStampedReference クラスが提供されています。このクラスの compareAndSet メソッドは、現在の参照が期待される参照と等しく、現在のフラグが期待されるフラグと等しいかを最初に確認し、すべてが等しい場合、原子方式でその参照とフラグの値を指定された更新値に設定します。

  1. 同期不要の方案
    スレッドセーフを保証するために、必ずしも同期が必要なわけではありません。両者には因果関係はありません。同期は、共有データの競合時の正しさを保証する手段に過ぎません。もしメソッドが共有データに関与しないのであれば、自然に正しさを保証するために同期操作を行う必要はありません。したがって、いくつかのコードは生まれつきスレッドセーフです。

1)再入可能コード
再入可能コード(ReentrantCode)は、純粋なコード(Pure Code)とも呼ばれ、コードの実行中にいつでも中断し、別のコードを実行することができ、制御が戻ったときに元のプログラムにエラーが発生しません。すべての再入可能コードはスレッドセーフですが、すべてのスレッドセーフなコードが再入可能であるわけではありません。

再入可能コードの特徴は、ヒープに保存されているデータや共有システムリソースに依存せず、使用される状態量はパラメータから渡され、非再入可能なメソッドを呼び出さないことです。

(類似:synchronized はロックの再入機能を持っており、synchronized を使用する際に、スレッドがオブジェクトロックを取得した後、再度このオブジェクトロックを要求すると、再びそのオブジェクトのロックを取得できます。)

2)スレッドローカルストレージ
コード内で必要なデータが他のコードと共有される必要がある場合、これらの共有データのコードが同じスレッド内で実行されることを保証できるかどうかを確認します。もし保証できるのであれば、共有データの可視範囲を同じスレッド内に制限できます。このようにして、同期なしでもスレッド間でデータの競合問題が発生しないことを保証できます。

この特徴に合致するアプリケーションは少なくありません。ほとんどの消費キューを使用するアーキテクチャパターン(「生産者 - 消費者」パターンなど)は、製品の消費プロセスをできるだけ 1 つのスレッドで消費するようにします。最も重要なアプリケーションの 1 つは、古典的な Web インタラクションモデルにおける「1 つのリクエストに対して 1 つのサーバースレッド(Thread-per-Request)」の処理方法です。この処理方法の広範な適用により、多くの Web サーバーアプリケーションはスレッドローカルストレージを使用してスレッドセーフの問題を解決できます。

三、スレッドのライフサイクルと 5 つの基本状態

Java スレッドには 5 つの基本状態があります:
新規状態(New):スレッドオブジェクトが作成されると、新規状態に入ります。例:Thread t = new MyThread ();
準備状態(Runnable):スレッドオブジェクトの start () メソッドを呼び出すと、スレッドは準備状態に入ります。準備状態のスレッドは、このスレッドが準備が整っていて、いつでも CPU のスケジューリングを待っていることを示しており、start () を呼び出したからといってこのスレッドが実行されるわけではありません。
実行状態(Running):CPU が準備状態のスレッドをスケジューリングするとき、この時スレッドは実際に実行され、実行状態に入ります。
注:準備状態は実行状態に入る唯一の入口であり、スレッドが実行状態に入る前提はすでに準備状態に入っていることです。
ブロック状態(Blocked):実行状態のスレッドが何らかの理由で CPU の使用権を一時的に放棄し、実行を停止すると、ブロック状態に入ります。この状態から準備状態に入ると、再び CPU によって呼び出されて実行状態に入る機会があります。ブロック状態の原因によって、ブロック状態は 3 つに分けられます:
待機ブロック→実行状態のスレッドが wait () メソッドを実行し、スレッドがブロック状態に入ります。
同期ブロック→スレッドが同期ロックを取得できず、他のスレッドによって同期ロックが占有されているため、スレッドは同期ブロック状態に入ります。
その他のブロック→スレッドが sleep () または join () を呼び出したり、I/O リクエストを発行したりすると、スレッドはブロック状態に入ります。sleep () 状態がタイムアウトしたり、join () が待機スレッドを終了させたり、タイムアウトしたり、I/O 処理が完了すると、スレッドは再び準備状態に戻ります。
死亡状態(Dead):スレッドが実行を完了するか、run () メソッドを終了したため、そのスレッドはライフサイクルを終了します。

参考コードはGiteeを参照してください。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。