1. Pengenalan
Dalam artikel ini, kita akan melihat salah satu mekanisme paling asas dalam Java - penyegerakan utas.
Mula-mula kita akan membincangkan beberapa istilah dan metodologi berkaitan bersama.
Dan kami akan mengembangkan aplikasi mudah - di mana kami akan menangani masalah serentak, dengan tujuan untuk memahami dengan lebih baik menunggu () dan memberitahu ().
2. Penyegerakan Thread di Java
Dalam persekitaran berbilang benang, beberapa utas mungkin cuba mengubah sumber yang sama. Sekiranya utas tidak dikendalikan dengan betul, ini tentu saja akan menimbulkan masalah konsistensi.
2.1. Blok Terpelihara di Jawa
Satu alat yang dapat kita gunakan untuk mengkoordinasikan tindakan beberapa utas di Java - blok yang dijaga. Blok semacam itu memeriksa keadaan tertentu sebelum meneruskan pelaksanaannya.
Dengan ini, kami akan memanfaatkan:
- Object.wait () - untuk menangguhkan utas
- Object.notify () - untuk membangunkan urutan
Ini dapat difahami dengan lebih baik dari rajah berikut, yang menggambarkan kitaran hidup Thread :

Harap maklum bahawa terdapat banyak cara untuk mengawal kitaran hidup ini; namun, dalam artikel ini, kita hanya akan fokus pada menunggu () dan memberitahu ().
3. tunggu () Menghubungi
Ringkasnya, ketika kita memanggil tunggu () - ini memaksa utas semasa menunggu sehingga beberapa utas lain meminta memberitahu () atau notifyAll () pada objek yang sama.
Untuk ini, utas semasa mesti memiliki monitor objek. Menurut Javadocs, ini boleh berlaku apabila:
- kami telah melaksanakan kaedah contoh yang disegerakkan untuk objek yang diberikan
- kami telah melaksanakan sekumpulan blok yang disegerakkan pada objek yang diberikan
- dengan melaksanakan statik disegerakkan kaedah untuk objek jenis Kelas
Perhatikan bahawa hanya satu utas aktif yang dapat memiliki monitor objek pada satu masa.
Ini menunggu () kaedah datang dengan tiga tandatangan muatan. Mari kita lihat ini.
3.1. tunggu ()
Kaedah tunggu () menyebabkan utas semasa menunggu selama-lamanya sehingga utas lain sama ada meminta pemberitahuan () untuk objek ini atau memberitahu semua () .
3.2. tunggu (jangka masa panjang)
Dengan menggunakan kaedah ini, kita dapat menentukan batas waktu yang mana benang akan terbangun secara automatik. Suatu utas boleh dibangunkan sebelum mencapai batas waktu menggunakan notify () atau notifyAll ().
Perhatikan bahawa panggilan menunggu (0) sama dengan panggilan menunggu ().
3.3. tunggu (timeout panjang, int nanos)
Ini adalah satu lagi tanda yang memberikan fungsi yang sama, dengan satu-satunya perbezaan ialah kita dapat memberikan ketepatan yang lebih tinggi.
Tempoh waktu tamat keseluruhan (dalam nanodetik), dikira sebagai 1_000_000 * tamat masa + nanos.
4. maklumkan () dan maklumkanSemua ()
Kaedah notify () digunakan untuk membangunkan benang yang sedang menunggu akses ke monitor objek ini.
Terdapat dua cara untuk memberitahu urutan menunggu.
4.1. memberitahu ()
Untuk semua utas yang menunggu di monitor objek ini (dengan menggunakan salah satu kaedah tunggu () ), kaedah memberitahu () memberitahu salah satu dari mereka untuk bangun dengan sewenang-wenangnya. Pemilihan benang yang tepat untuk bangun tidak bersifat deterministik dan bergantung pada pelaksanaannya.
Oleh kerana memberitahu () membangunkan satu utas rawak, ia dapat digunakan untuk melaksanakan penguncian yang saling eksklusif di mana utas melakukan tugas yang serupa, tetapi dalam kebanyakan kes, lebih berguna untuk menerapkan notifyAll () .
4.2. maklumkanSemua ()
Kaedah ini hanya membangunkan semua utas yang menunggu di monitor objek ini.
Benang yang terbangun akan selesai dengan cara biasa - seperti benang lain.
Tetapi sebelum kita membiarkan pelaksanaannya berlanjutan, selalu tentukan pemeriksaan cepat untuk syarat yang diperlukan untuk meneruskan utas - kerana mungkin ada beberapa situasi di mana utas terbangun tanpa menerima pemberitahuan (senario ini dibincangkan kemudian dalam contoh) .
5. Masalah Penyegerakan Penghantar-Penerima
Sekarang setelah kita memahami asasnya, mari kita melalui aplikasi Pengirim - Penerima yang mudah - yang akan menggunakan kaedah menunggu () dan memberitahu () untuk mengatur penyegerakan di antara mereka:
- The Sender sepatutnya menghantar paket data kepada Penerima
- The Penerima tidak dapat memproses paket data sehingga Sender selesai menghantarnya
- Begitu juga, Pengirim tidak boleh berusaha untuk menghantar paket lain kecuali Penerima telah memproses paket sebelumnya
Let's first create Data class that consists of the data packet that will be sent from Sender to Receiver. We'll use wait() and notifyAll() to set up synchronization between them:
public class Data { private String packet; // True if receiver should wait // False if sender should wait private boolean transfer = true; public synchronized void send(String packet) { while (!transfer) { try { wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Log.error("Thread interrupted", e); } } transfer = false; this.packet = packet; notifyAll(); } public synchronized String receive() { while (transfer) { try { wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Log.error("Thread interrupted", e); } } transfer = true; notifyAll(); return packet; } }
Let's break down what's going on here:
- The packet variable denotes the data that is being transferred over the network
- We have a boolean variable transfer – which the Sender and Receiver will use for synchronization:
- If this variable is true, then the Receiver should wait for Sender to send the message
- If it's false, then Sender should wait for Receiver to receive the message
- The Sender uses send() method to send data to the Receiver:
- If transfer is false, we'll wait by calling wait() on this thread
- But when it is true, we toggle the status, set our message and call notifyAll() to wake up other threads to specify that a significant event has occurred and they can check if they can continue execution
- Similarly, the Receiver will use receive() method:
- If the transfer was set to false by Sender, then only it will proceed, otherwise we'll call wait() on this thread
- When the condition is met, we toggle the status, notify all waiting threads to wake up and return the data packet that was Receiver
5.1. Why Enclose wait() in a while Loop?
Since notify() and notifyAll() randomly wakes up threads that are waiting on this object's monitor, it's not always important that the condition is met. Sometimes it can happen that the thread is woken up, but the condition isn't actually satisfied yet.
We can also define a check to save us from spurious wakeups – where a thread can wake up from waiting without ever having received a notification.
5.2. Why Do We Need to Synchronize send() and receive() Methods?
We placed these methods inside synchronized methods to provide intrinsic locks. If a thread calling wait() method does not own the inherent lock, an error will be thrown.
We'll now create Sender and Receiver and implement the Runnable interface on both so that their instances can be executed by a thread.
Let's first see how Sender will work:
public class Sender implements Runnable { private Data data; // standard constructors public void run() { String packets[] = { "First packet", "Second packet", "Third packet", "Fourth packet", "End" }; for (String packet : packets) { data.send(packet); // Thread.sleep() to mimic heavy server-side processing try { Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Log.error("Thread interrupted", e); } } } }
For this Sender:
- We're creating some random data packets that will be sent across the network in packets[] array
- For each packet, we're merely calling send()
- Then we're calling Thread.sleep() with random interval to mimic heavy server-side processing
Finally, let's implement our Receiver:
public class Receiver implements Runnable { private Data load; // standard constructors public void run() { for(String receivedMessage = load.receive(); !"End".equals(receivedMessage); receivedMessage = load.receive()) { System.out.println(receivedMessage); // ... try { Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Log.error("Thread interrupted", e); } } } }
Here, we're simply calling load.receive() in the loop until we get the last “End” data packet.
Let's now see this application in action:
public static void main(String[] args) { Data data = new Data(); Thread sender = new Thread(new Sender(data)); Thread receiver = new Thread(new Receiver(data)); sender.start(); receiver.start(); }
We'll receive the following output:
First packet Second packet Third packet Fourth packet
And here we are – we've received all data packets in the right, sequential order and successfully established the correct communication between our sender and receiver.
6. Conclusion
In this article, we discussed some core synchronization concepts in Java; more specifically, we focused on how we can use wait() and notify() to solve interesting synchronization problems. And finally, we went through a code sample where we applied these concepts in practice.
Before we wind down here, it's worth mentioning that all these low-level APIs, such as wait(), notify() and notifyAll() – are traditional methods that work well, but higher-level mechanism are often simpler and better – such as Java's native Lock and Condition interfaces (available in java.util.concurrent.locks package).
Untuk maklumat lebih lanjut mengenai pakej java.util.concurrent , kunjungi gambaran keseluruhan artikel java.util.concurrent kami, dan Kunci dan Keadaan diliputi dalam panduan untuk java.util.concurrent.Locks, di sini.
Seperti biasa, coretan kod lengkap yang digunakan dalam artikel ini terdapat di GitHub.