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
詳細について用のドキュメントを参照してくださいExecutor
、ExecutorService
とExecutors
。
イベント処理
ユーザーが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チュートリアルの同時実行セクションも参照してください。マルチスレッドは複雑ですが、利用できるヘルプはたくさんあります。