web-dev-qa-db-ja.com

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

私のコードには、ある状態が別のスレッドから変更されるのを待つループがあります。他のスレッドは機能しますが、私のループは変更された値を見ることはありません。 それは永遠に待機します。しかし、ループに_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");ステートメントのコメントを外したときにのみ機能します。どうしたの?

82
Boann

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

while (pizzaArrived == false) {}

これに:

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

これは無限ループです。

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

volatile boolean pizzaArrived = false;

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

(より正式には、volatileは変数へのアクセス間にhappens-before関係を作成します。これは スレッドが行った他のすべての作業 を意味しますピザを配達する前は、ピザを受け取ったスレッドからも見ることができます。これらの変更はvolatile変数に対するものではありません。)

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

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.outPrintStreamオブジェクトです。 PrintStreamのメソッドは次のように同期されます。

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

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

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


より大きな問題

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

以下にその方法をいくつか示します。

待機/通知を使用する

低レベルの解決策は Objectのwait/notifyメソッドを使用 :です。

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を呼び出す場合、そのオブジェクトの同期ロックを保持する必要があります。これは上記のコードが行うことです。両方のスレッドが同じオブジェクトを使用する限り、任意のオブジェクトを使用できます。ここでは、thisMyHouseのインスタンス)を使用しました。通常、2つのスレッドは同じオブジェクトの同期ブロックに同時に入ることはできません(同期の目的の一部です)が、wait()内にあるときにスレッドが同期ロックを一時的に解除するため、ここで動作します方法。

BlockingQueue

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");
    }
}

注:puttakeおよびBlockingQueueメソッドは、InterruptedExceptionsをスローできます。これらは、処理する必要があるチェック済み例外です。上記のコードでは、簡単にするために、例外は再スローされます。メソッドで例外をキャッチし、putまたはtake呼び出しを再試行して、成功することを確認することをお勧めします。 oneさの一点を除けば、BlockingQueueは非常に使いやすいです。

BlockingQueueは、アイテムをキューに入れる前にスレッドが行ったすべてのことを、それらのアイテムを取り出すスレッドに見えるようにするため、ここでは他の同期は必要ありません。

執行者

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

// 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

詳細については、 ExecutorExecutorService 、および Executors のドキュメントを参照してください。

イベント処理

ユーザーがUIで何かをクリックするのを待っている間にループするのは間違っています。代わりに、UIツールキットのイベント処理機能を使用してください。 In 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とのその他の対話がブロックされます。遅い操作は、新しいスレッドで開始するか、上記の手法(wait/notify、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には、スケジュールされたTimerTasksの実行に使用される独自のバックグラウンドスレッドがあります。当然、スレッドはタスク間でスリープするため、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チュートリアル。 同時実行性セクション もご覧ください。マルチスレッドは複雑ですが、多くのヘルプが利用可能です!

131
Boann