ループは、printステートメントのない他のスレッドによって変更された値を認識しません


89

私のコードには、別のスレッドから状態が変更されるのを待つループがあります。他のスレッドは機能しますが、私のループは変更された値を認識しません。永遠に待ちます。しかし、System.out.printlnステートメントをループに入れると、突然動作します!どうして?


以下は私のコードの例です:

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        while (pizzaArrived == false) {
            //System.out.println("waiting");
        }

        System.out.println("That was delicious!");
    }

    void deliverPizza() {
        pizzaArrived = true;
    }
}

whileループの実行中にdeliverPizza()、別のスレッドから呼び出してpizzaArrived変数を設定します。しかし、ループは、System.out.println("waiting");ステートメントのコメントを外したときにのみ機能します。どうしたの?

回答:


148

JVMは、他のスレッドがpizzaArrivedループ中に変数を変更しないと想定することができます。言い換えれば、pizzaArrived == falseループの外でテストを引き上げ、これを最適化できます。

while (pizzaArrived == false) {}

これに:

if (pizzaArrived == false) while (true) {}

これは無限ループです。

あるスレッドによって行われた変更が他のスレッドから見えるようにするには、常にスレッド間の同期を追加する必要があります。これを行う最も簡単な方法は、共有変数を作成することですvolatile

volatile boolean pizzaArrived = false;

変数を作成volatileすると、異なるスレッドが互いに対する変更の影響を確実に認識することができます。これにより、JVMが値をキャッシュしpizzaArrivedたり、ループの外でテストを引き上げたりすることを防ぎます。代わりに、毎回実変数の値を読み取る必要があります。

(より正式にvolatileは、変数へのアクセスの間に発生前の関係を作成します。これは、他の変更が変数に適用されない場合でも、ピザを配信する前にスレッドが行った他のすべての作業も、ピザを受け取るスレッドに表示されることを意味しvolatileます。)

同期化されたメソッドは、主に相互排除(同時に発生する2つのことを防ぐ)を実装するために使用されますが、それらにはすべて同じ副作用がvolatileあります。変数を読み書きするときにそれらを使用することは、他のスレッドに変更を可視化するもう1つの方法です。

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        while (getPizzaArrived() == false) {}
        System.out.println("That was delicious!");
    }

    synchronized boolean getPizzaArrived() {
        return pizzaArrived;
    }

    synchronized void deliverPizza() {
        pizzaArrived = true;
    }
}

印刷ステートメントの効果

System.outあるPrintStreamオブジェクトが。のメソッドはPrintStream次のように同期されます。

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

同期によりpizzaArrived、ループ中にキャッシュされることが防止されます。厳密に言えば、変数への変更が確実に表示されるように両方のスレッドが同じオブジェクトで同期する必要があります。(たとえば、println設定後に呼び出し、pizzaArrived読み取り前に再度呼び出すことpizzaArrivedは正しいでしょう。)特定のオブジェクトで同期するスレッドが1つだけの場合、JVMはそれを無視できます。実際には、JVMはprintln、の設定後に他のスレッドが呼び出されないことを証明するほどスマートではないpizzaArrivedため、そうであると想定しています。したがって、を呼び出しSystem.out.printlnた場合、ループ中に変数をキャッシュできません。このため、このようなループは、printステートメントがある場合に機能しますが、正しい修正ではありません。

System.outこの効果を引き起こす唯一の方法はを使用することではありませんが、ループが機能しない理由をデバッグしようとするときに最も頻繁に発見される方法です。


より大きな問題

while (pizzaArrived == false) {}ビジー待機ループです。それは良くないね!待機中は、CPUを占有するため、他のアプリケーションの速度が低下し、システムの電力使用量、温度、ファン速度が増加します。理想的には、待機中にループスレッドをスリープさせて、CPUを占有しないようにします。

これを行ういくつかの方法を次に示します。

待機/通知の使用

低レベルのソリューションは、次の待機/通知メソッドObject使用することです

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        synchronized (this) {
            while (!pizzaArrived) {
                try {
                    this.wait();
                } catch (InterruptedException e) {}
            }
        }

        System.out.println("That was delicious!");
    }

    void deliverPizza() {
        synchronized (this) {
            pizzaArrived = true;
            this.notifyAll();
        }
    }
}

このバージョンのコードでは、ループスレッドはを呼び出しwait()、スレッドをスリープ状態にします。スリープ中はCPUサイクルを使用しません。2番目のスレッドは変数を設定した後、notifyAll()そのオブジェクトを待機していたすべてのスレッドを起動するために呼び出します。これは、ピザの男がドアベルを鳴らすようなものなので、ドアにぎこちなく立っている代わりに、座って待っている間休むことができます。

オブジェクトでwait / notifyを呼び出すときは、そのオブジェクトの同期ロックを保持する必要があります。これは、上記のコードが行うことです。両方のスレッドが同じオブジェクトを使用する限り、好きなオブジェクトを使用できます。ここでは私が使用しましたthis(のインスタンスMyHouse)。通常、2つのスレッドは同じオブジェクトの同期ブロックに同時に入ることはできませんが(同期の目的の一部です)、スレッドがwait()メソッド内にあるときに一時的に同期ロックを解放するため、ここで機能します。

BlockingQueue

A BlockingQueueは、プロデューサー-コンシューマーキューを実装するために使用されます。「消費者」は列の前からアイテムを取り、「生産者」は後ろからアイテムを押します。例:

class MyHouse {
    final BlockingQueue<Object> queue = new LinkedBlockingQueue<>();

    void eatFood() throws InterruptedException {
        // take next item from the queue (sleeps while waiting)
        Object food = queue.take();
        // and do something with it
        System.out.println("Eating: " + food);
    }

    void deliverPizza() throws InterruptedException {
        // in producer threads, we push items on to the queue.
        // if there is space in the queue we can return immediately;
        // the consumer thread(s) will get to it later
        queue.put("A delicious pizza");
    }
}

注:putおよびのtakeメソッドはs BlockingQueueをスローできInterruptedExceptionます。これは、処理する必要があるチェック済み例外です。上記のコードでは、簡単にするために、例外が再スローされています。メソッドで例外をキャッチし、成功したことを確認するためにputまたはtake呼び出しを再試行することをお勧めします。醜さのその1つのポイントは別として、BlockingQueue非常に使いやすいです。

BlockingQueueキューにアイテムを置く前にスレッドが行ったすべてのことを確認するため、他の同期は必要ありません。

執行者

Executorは、BlockingQueueタスクを実行する既製のに似ています。例:

// A "SingleThreadExecutor" has one work thread and an unlimited queue
ExecutorService executor = Executors.newSingleThreadExecutor();

Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };
Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); };

// we submit tasks which will be executed on the work thread
executor.execute(eatPizza);
executor.execute(cleanUp);
// we continue immediately without needing to wait for the tasks to finish

詳細について用のドキュメントを参照してくださいExecutorExecutorServiceExecutors

イベント処理

ユーザーがUIで何かをクリックするのを待つ間にループするのは間違っています。代わりに、UIツールキットのイベント処理機能を使用してください。Swingでは、例えば:

JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
    // This event listener is run when the button is clicked.
    // We don't need to loop while waiting.
    label.setText("Button was clicked");
});

イベントハンドラーはイベントディスパッチスレッドで実行されるため、イベントハンドラーで長時間の作業を行うと、作業が完了するまでUIとの他のやり取りがブロックされます。遅い操作は、新しいスレッドで開始するか、上記の方法(待機/通知、、BlockingQueueまたはExecutor)のいずれかを使用して待機中のスレッドにディスパッチできます。SwingWorkerこのために設計され、バックグラウンドワーカースレッドを自動的に提供するを使用することもできます。

JLabel label = new JLabel();
JButton button = new JButton("Calculate answer");

// Add a click listener for the button
button.addActionListener((ActionEvent e) -> {

    // Defines MyWorker as a SwingWorker whose result type is String:
    class MyWorker extends SwingWorker<String,Void> {
        @Override
        public String doInBackground() throws Exception {
            // This method is called on a background thread.
            // You can do long work here without blocking the UI.
            // This is just an example:
            Thread.sleep(5000);
            return "Answer is 42";
        }

        @Override
        protected void done() {
            // This method is called on the Swing thread once the work is done
            String result;
            try {
                result = get();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            label.setText(result); // will display "Answer is 42"
        }
    }

    // Start the worker
    new MyWorker().execute();
});

タイマー

定期的なアクションを実行するには、を使用できますjava.util.Timer。独自のタイミングループを作成するよりも使いやすく、開始と停止が簡単です。このデモは、1秒に1回現在時刻を出力します。

Timer timer = new Timer();
TimerTask task = new TimerTask() {
    @Override
    public void run() {
        System.out.println(System.currentTimeMillis());
    }
};
timer.scheduleAtFixedRate(task, 0, 1000);

それぞれにjava.util.Timer、スケジュールされたを実行するために使用される独自のバックグラウンドスレッドがありTimerTaskます。当然、スレッドはタスク間でスリープするため、CPUを占有することはありません。

Swingコードにもjavax.swing.Timer同様のがありますが、これはSwingスレッドでリスナーを実行するため、手動でスレッドを切り替える必要なく、Swingコンポーネントと安全に対話できます。

JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Timer timer = new Timer(1000, (ActionEvent e) -> {
    frame.setTitle(String.valueOf(System.currentTimeMillis()));
});
timer.setRepeats(true);
timer.start();
frame.setVisible(true);

他の方法

マルチスレッドコードを記述している場合は、これらのパッケージのクラスを調べて、何が利用できるかを確認することをお勧めします。

また、Javaチュートリアルの同時実行セクションも参照してください。マルチスレッドは複雑ですが、利用できるヘルプはたくさんあります。


非常に専門的な回答です。これを読んだ後、誤解はありません。ありがとう
Humoyun Ahmad

1
素晴らしい答え。私はかなり長い間Javaスレッドを使用していますwait()が、ここで何かを学びました(同期ロックを解放します!)。
ブリムボリウム

ボアン、ありがとう!正解です。例のある記事のようなものです。はい、「wait()が同期ロックを解放する」も気に入っています
Kiryl Ivanou

java public class ThreadTest { private static boolean flag = false; private static class Reader extends Thread { @Override public void run() { while(flag == false) {} System.out.println(flag); } } public static void main(String[] args) { new Reader().start(); flag = true; } } @Boann、このコードはpizzaArrived == falseループの外でテストを引き上げず、ループはメインスレッドによって変更されたフラグを確認できます。なぜですか?
gaussclb

1
@gaussclbクラスファイルを逆コンパイルしたということであれば、修正してください。Javaコンパイラーは最適化をほとんど行いません。巻き上げはJVMによって行われます。ネイティブマシンコードを逆アセンブルする必要があります。試してみてください:wiki.openjdk.java.net/display/HotSpot/PrintAssembly
Boann
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.