JVM diizinkan untuk menganggap bahwa utas lain tidak mengubah pizzaArrived
variabel selama pengulangan. Dengan kata lain, ini dapat mengangkat pizzaArrived == false
tes di luar loop, mengoptimalkan ini:
while (pizzaArrived == false) {}
ke dalam ini:
if (pizzaArrived == false) while (true) {}
yang merupakan loop tak terbatas.
Untuk memastikan bahwa perubahan yang dibuat oleh satu utas terlihat oleh utas lain, Anda harus selalu menambahkan beberapa sinkronisasi antar utas. Cara termudah untuk melakukannya adalah dengan membuat variabel bersama volatile
:
volatile boolean pizzaArrived = false;
Membuat variabel volatile
menjamin bahwa utas yang berbeda akan melihat efek perubahan satu sama lain terhadapnya. Ini mencegah JVM dari menyimpan nilai pizzaArrived
atau mengangkat pengujian di luar loop. Sebaliknya, ia harus membaca nilai variabel riil setiap saat.
(Secara lebih formal, volatile
membuat hubungan terjadi-sebelum antara akses ke variabel. Ini berarti bahwa semua pekerjaan lain yang dilakukan utas sebelum mengirim pizza juga terlihat oleh utas yang menerima pizza, meskipun perubahan lain tersebut bukan pada volatile
variabel.)
Metode tersinkronisasi digunakan pada prinsipnya untuk menerapkan pengecualian timbal balik (mencegah dua hal terjadi pada saat yang sama), tetapi metode tersebut juga memiliki semua efek samping yang sama volatile
. Menggunakannya saat membaca dan menulis variabel adalah cara lain untuk membuat perubahan terlihat ke utas lain:
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;
}
}
Efek dari pernyataan cetak
System.out
adalah sebuah PrintStream
objek. Metode PrintStream
disinkronkan seperti ini:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
Sinkronisasi mencegah pizzaArrived
cache selama loop. Sebenarnya, kedua utas harus disinkronkan pada objek yang sama untuk menjamin bahwa perubahan pada variabel terlihat. (Misalnya, memanggil println
setelah menyetel pizzaArrived
dan memanggilnya lagi sebelum membaca pizzaArrived
akan benar.) Jika hanya satu utas yang disinkronkan pada objek tertentu, JVM diizinkan untuk mengabaikannya. Dalam praktiknya, JVM tidak cukup pintar untuk membuktikan bahwa utas lain tidak akan memanggil println
setelah pengaturan pizzaArrived
, jadi diasumsikan bahwa mereka mungkin. Oleh karena itu, ia tidak dapat menyimpan variabel selama loop jika Anda memanggil System.out.println
. Itulah mengapa loop seperti ini berfungsi ketika mereka memiliki pernyataan cetak, meskipun itu bukan perbaikan yang benar.
Menggunakan System.out
bukan satu-satunya cara untuk menyebabkan efek ini, tetapi ini adalah yang paling sering ditemukan orang, ketika mereka mencoba men-debug mengapa loop mereka tidak berfungsi!
Masalah yang lebih besar
while (pizzaArrived == false) {}
adalah loop tunggu sibuk. Itu buruk! Sementara menunggu, itu memonopoli CPU, yang memperlambat aplikasi lain, dan meningkatkan penggunaan daya, suhu, dan kecepatan kipas sistem. Idealnya, kami ingin utas loop tidur sementara menunggu, sehingga tidak membebani CPU.
Berikut beberapa cara untuk melakukannya:
Menggunakan wait / notify
Solusi tingkat rendah adalah dengan menggunakan metode tunggu / beri tahu dariObject
:
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();
}
}
}
Dalam versi kode ini, thread loop memanggil wait()
, yang membuat thread tersebut tidur. Ini tidak akan menggunakan siklus CPU apa pun saat tidur. Setelah utas kedua menyetel variabel, ia memanggil notifyAll()
untuk membangunkan setiap / semua utas yang menunggu di objek itu. Ini seperti menyuruh tukang pizza membunyikan bel pintu, jadi Anda bisa duduk dan beristirahat sambil menunggu, alih-alih berdiri dengan canggung di depan pintu.
Saat memanggil wait / notify pada suatu objek, Anda harus menahan kunci sinkronisasi dari objek itu, yang dilakukan oleh kode di atas. Anda dapat menggunakan objek apa pun yang Anda suka selama kedua utas menggunakan objek yang sama: di sini saya menggunakan this
(contoh MyHouse
). Biasanya, dua utas tidak akan dapat masuk ke blok tersinkronisasi dari objek yang sama secara bersamaan (yang merupakan bagian dari tujuan sinkronisasi) tetapi berfungsi di sini karena utas melepaskan kunci sinkronisasi untuk sementara ketika berada di dalam wait()
metode.
BlockingQueue
A BlockingQueue
digunakan untuk mengimplementasikan antrian produsen-konsumen. "Konsumen" mengambil item dari depan antrean, dan "produsen" mendorong item di belakang. Sebuah contoh:
class MyHouse {
final BlockingQueue<Object> queue = new LinkedBlockingQueue<>();
void eatFood() throws InterruptedException {
Object food = queue.take();
System.out.println("Eating: " + food);
}
void deliverPizza() throws InterruptedException {
queue.put("A delicious pizza");
}
}
Catatan: Metode put
and take
dari BlockingQueue
can throw InterruptedException
s, yang merupakan pengecualian yang diperiksa yang harus ditangani. Dalam kode di atas, untuk mempermudah, pengecualian dicantumkan kembali. Anda mungkin lebih suka menangkap pengecualian dalam metode dan mencoba kembali put atau take untuk memastikannya berhasil. Terlepas dari satu titik keburukan itu, BlockingQueue
sangat mudah digunakan.
Tidak ada sinkronisasi lain yang diperlukan di sini karena a BlockingQueue
memastikan bahwa semua yang dilakukan utas sebelum meletakkan item dalam antrean terlihat oleh utas yang mengeluarkan item tersebut.
Pelaksana
Executor
s seperti s siap pakai BlockingQueue
yang menjalankan tugas. Contoh:
ExecutorService executor = Executors.newSingleThreadExecutor();
Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };
Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); };
executor.execute(eatPizza);
executor.execute(cleanUp);
Untuk rincian lihat doc untuk Executor
, ExecutorService
, dan Executors
.
Penanganan acara
Pengulangan sambil menunggu pengguna mengklik sesuatu di UI adalah salah. Sebagai gantinya, gunakan fitur penanganan acara dari toolkit UI. Di Swing , misalnya:
JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
label.setText("Button was clicked");
});
Karena event handler berjalan pada event dispatch thread, melakukan pekerjaan yang lama di event handler memblokir interaksi lain dengan UI hingga pekerjaan selesai. Operasi lambat dapat dimulai pada thread baru, atau dikirim ke thread menunggu menggunakan salah satu teknik di atas (tunggu / beri tahu, a BlockingQueue
, atau Executor
). Anda juga dapat menggunakan a SwingWorker
, yang dirancang persis untuk ini, dan secara otomatis menyediakan thread pekerja latar belakang:
JLabel label = new JLabel();
JButton button = new JButton("Calculate answer");
button.addActionListener((ActionEvent e) -> {
class MyWorker extends SwingWorker<String,Void> {
@Override
public String doInBackground() throws Exception {
Thread.sleep(5000);
return "Answer is 42";
}
@Override
protected void done() {
String result;
try {
result = get();
} catch (Exception e) {
throw new RuntimeException(e);
}
label.setText(result);
}
}
new MyWorker().execute();
});
Timer
Untuk melakukan tindakan berkala, Anda dapat menggunakan file java.util.Timer
. Ini lebih mudah digunakan daripada menulis putaran waktu Anda sendiri, dan lebih mudah untuk memulai dan menghentikan. Demo ini mencetak waktu saat ini sekali per detik:
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
};
timer.scheduleAtFixedRate(task, 0, 1000);
Masing java.util.Timer
-masing memiliki utas latar belakangnya sendiri yang digunakan untuk menjalankan jadwal yang dijadwalkan TimerTask
. Secara alami, utas tidur di antara tugas-tugas, sehingga tidak membebani CPU.
Dalam kode Swing, ada juga a javax.swing.Timer
, yang serupa, tetapi menjalankan listener pada thread Swing, sehingga Anda dapat berinteraksi dengan aman dengan komponen Swing tanpa perlu mengganti thread secara manual:
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);
Cara lain
Jika Anda menulis kode multithread, ada baiknya menjelajahi kelas-kelas dalam paket ini untuk melihat apa yang tersedia:
Dan juga lihat bagian Concurrency dari tutorial Java. Multithreading itu rumit, tetapi ada banyak bantuan yang tersedia!