マルチスレッド - 2 つの単純な Java スレッドがインターリーブ アクセスを取得できないのはなぜですか?

okwaves2024-01-25  11

スレッド インターリーブと Java スレッド スケジューリングに関する投稿を読みましたが、排他的なリソースに対して実行されている 2 つの単純な Java スレッドがリソースへのインターリーブ アクセスをある程度取得できない理由がわかりません。

以下のコードには、同期ブロックを含む唯一の静的 access() メソッドを持つ Resource クラスがあります。メインで 2 つの MyThread インスタンスを作成して開始するだけです。各スレッドは Resource.access() への呼び出しを繰り返しループします。すべてのアクセスは 1 秒間スリープし、その後リソースが解放されます。

public class X {
    public static void main(String[] args) {
        System.out.println("Creating threads");

        Thread t1 = (new Thread(new MyThread()));
        System.out.println(String.format("Thread t1 id = %d",
            t1.getId()));

        Thread t2 = (new Thread(new MyThread()));
        System.out.println(String.format("Thread t2 id = %d",
            t2.getId()));

        System.out.println("Starting threads");
        t1.start();
        t2.start();
    }
}

class MyThread implements Runnable {    
    @Override
    public void run () {
        for (int i = 0; i <= 5; i++) {
            Resource.access();
        }
        System.out.println(String.format("Thread %d completed",
            Thread.currentThread().getId()));
    }
}

class Resource {
    private static Object monitor = new Object();

    public static void access() {
        synchronized (monitor) {
            System.out.println(String.format(
                "Thread %d accessing resource", 
                Thread.currentThread().getId()));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ie) {
                ie.printStackTrace();
                System.exit(1);
            }
            System.out.println(String.format(
                "Thread %d leaving resource", 
                Thread.currentThread().getId()));
        }
    } 
}

しかし、何を試しても、最初に開始された MyThread インスタンス (上の例では t1) のループが、他の MyThread インスタンス (上の例では t2) がリソースへのアクセスを取得する前に、リソースへのすべてのアクセスを完了します。 。彼re は出力です:

スレッドの作成 スレッド t1 ID = 12 スレッド t2 ID = 13 スレッドの開始 スレッド 12 がリソースにアクセスしています スレッド 12 がリソースを離れる スレッド 12 がリソースにアクセスしています スレッド 12 がリソースを離れる スレッド 12 がリソースにアクセスしています スレッド 12 がリソースを離れる スレッド 12 がリソースにアクセスしています スレッド 12 がリソースを離れる スレッド 12 がリソースにアクセスしています スレッド 12 がリソースを離れる スレッド 12 がリソースにアクセスしています スレッド 12 がリソースを離れる スレッド 13 がリソースにアクセスしています スレッド12が完了しました スレッド 13 がリソースを離れる スレッド 13 がリソースにアクセスしています スレッド 13 がリソースを離れる スレッド 13 がリソースにアクセスしています スレッド 13 がリソースを離れる スレッド 13 がリソースにアクセスしています スレッド 13 がリソースを離れる スレッド 13 がリソースにアクセスしています スレッド 13 がリソースを離れる スレッド 13 がリソースにアクセスしています スレッド 13 がリソースを離れる スレッド 13 コム完了しました

通常の優先順位の Java スレッドは最大 1 秒のタイム スライスを取得する可能性があると読みました。そのため、上記のスレッド 13 (t2) がリソースにインターリーブ アクセスを持つことを期待します。必ずしも 1 対 1 ではなく、ある程度のインターリーブが必要です。 .



------------------------

スリープ ステートメントを同期ブロックの外に移動して、他のスレッドにアクセスする機会を与えます。そうしないと、モニターが解放されたときに 1 つのスレッドがメソッドにアクセスできる非常に小さな機会に依存することになります。

もう 1 つの方法は、スリープをそのままにし、継続時間を 1000 ミリ秒から 100 ミリ秒に変更することです。次に、ループを 5 から 5000 に増やします。.

結局のところ、スレッド リソースの競合は、必ずしも予想どおりに現れるわけではないということです。スレッドは、対話の成功を示すために常に協力するとは限りません。

class Resource {
    private static Object monitor = new Object();
    
    public static void access() {
        synchronized (monitor) {
            System.out.println(
                    String.format("Thread %d accessing resource",
                            Thread.currentThread().getId()));
            System.out.println(
                    String.format("Thread %d leaving resource",
                            Thread.currentThread().getId()));
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ie) {
            ie.printStackTrace();
            System.exit(1);
        }
    }

1

説明に追加すると、一度に 1 つだけアクセスできるリソースのみを同期するようにしてください。そうしないと、スレッドが不必要にブロックされてしまいます。

– ガガルワ

2020 年 9 月 5 日 1:58



------------------------

TL;DR : パフォーマンス上の理由から、Java の synchronized() は公平なロックを使用しません。 synchronized() ブロックを ReentrantLock に置き換え、「fair」に true を渡します。 ReentrantLock のコンストラクターへの引数。ただし、以下の説明を参照してください。

これは効率の問題です。スレッド内で何が起こっているかは次のとおりです。

両方のスレッドが開始されます。そのうちの 1 人がロックを取得します。もう 1 つはロックを待っている間スリープ状態になります。どちらの方法でも構いませんが、ロックを取得するものをスレッド A と呼びましょう。 スレッド A はリソースを 1 秒間使用します (ロック内の sleep() で表されます) スレッド A がロックを解放します。これ自体は特にコンテキストの切り替えをトリガーしません。スレッド A は引き続き実行されます。スレッド B はまだスリープ状態ですこの点。 ここでの CHANCE は、スレッド B がロックを取得すると予想されますが、スレッド B はスリープ状態です。以下を参照してください。 スレッド A には、次のロックのチェックまでに実行すべき命令がほとんどありません。それらを実行しますが、スレッド B はまだスリープ状態であるため、ロックが解放されていることを確認し、再度ロックを取得します。 これはスレッド A が完了するまで繰り返されます。

スレッド B が起動して CHANCE でロックを取得すると予想されることはわかりますが、それほど単純ではありません。これを行うには、スレッドが実行されてロックを取得しようとする必要があります。しかし、スリープしているのには正当な理由があります。ロックを待っているためにスリープ状態になっているのです。スレッド B には、ロックが解放されるとすぐにロックを取得しようとする準備ができているアイドル状態のコアがありません。これはリソースの無駄になるためです (マルチコア CPU 上でも、他のコアが別の処理を行っていて、他のコアが実行していない場合でも)電力を節約するためにシャットダウンされます)。 CHANCE では、これらのごく少数の命令でスレッド A がたまたまプリエンプトされ、その時点でスレッド B がたまたまウェイクアップされて先に進むことが技術的に可能です。スレッド A は再びロックできるようになるまで何もすることがほとんどないため、その可能性は非常に低いです。

そうは言っても、あなたの問題は他の人が抱えていた(わずかに)一般的な問題であり、それを解決するツール、つまりフェアロックがあります。公平なロックには、特にここで発生している種類の問題に対処するために、各スレッドがロックを取得するチャンスを均等にする戦略があります。 Java では、コンストラクターに true を渡すと、ReentrantLock がこれを実装します。

注: ロックには公平性を実装するコードが含まれているため、公平なロックを使用すると、パフォーマンスが若干低下します。スレッドがロックを取得するとき、ロックは他のスレッドが待機していることをチェックして、常に同じスレッドがロックを取得しているわけではないことを確認する必要があります。これが、 synchronized() がデフォルトで公平なロックを使用しない理由です。ロックに関するパフォーマンスと競合の問題は、公平性の問題よりもはるかに一般的です。 注: フェアロックは、あなたのようなケースでの注文を保証するものでもありません。彼らは、待機時間の長いスレッドに有利な確率を積み上げ、最終的にはスレッドに順番が回ってくることを保証します。詳細については、ReentrantLock に関するドキュメントを参照してください。

1

ブールフラグを使用すると、より予測可能なインターリーブ結果が得られます。 flag が true の場合、1 つのスレッドが処理を実行してフラグを切り替えます。flag が false の場合、2 番目のスレッドが処理を実行してフラグを切り替えます。

– イワン

2020 年 9 月 8 日 22:23

総合生活情報サイト - OKWAVES
総合生活情報サイト - OKWAVES
生活総合情報サイトokwaves(オールアバウト)。その道のプロ(専門家)が、日常生活をより豊かに快適にするノウハウから業界の最新動向、読み物コラムまで、多彩なコンテンツを発信。