Panduan CountDownLatch di Java

1. Pengenalan

Dalam artikel ini, kami akan memberikan panduan untuk kelas CountDownLatch dan menunjukkan bagaimana ia dapat digunakan dalam beberapa contoh praktikal.

Pada dasarnya, dengan menggunakan CountDownLatch kita boleh menyebabkan utas tersekat sehingga utas lain menyelesaikan tugas yang diberikan.

2. Penggunaan dalam Pengaturcaraan Serentak

Ringkasnya, CountDownLatch mempunyai medan penghitung , yang dapat anda tolak mengikut kehendak kami. Kita kemudian dapat menggunakannya untuk menyekat utas panggilan sehingga dihitung menjadi sifar.

Sekiranya kita melakukan pemprosesan selari, kita dapat memberi contoh CountDownLatch dengan nilai yang sama untuk kaunter dengan sebilangan utas yang ingin kita jalani. Kemudian, kita hanya dapat membuat undur panggilan () setelah setiap utas selesai, menjamin bahawa panggilan menunggu bergantung () akan menyekat sehingga utas pekerja selesai.

3. Menunggu Kolam Benang Selesai

Mari cuba corak ini dengan membuat Pekerja dan menggunakan medan CountDownLatch untuk memberi isyarat apabila sudah selesai:

public class Worker implements Runnable { private List outputScraper; private CountDownLatch countDownLatch; public Worker(List outputScraper, CountDownLatch countDownLatch) { this.outputScraper = outputScraper; this.countDownLatch = countDownLatch; } @Override public void run() { doSomeWork(); outputScraper.add("Counted down"); countDownLatch.countDown(); } }

Kemudian, mari buat ujian untuk membuktikan bahawa kita boleh mendapatkan CountDownLatch untuk menunggu masa pekerja selesai:

@Test public void whenParallelProcessing_thenMainThreadWillBlockUntilCompletion() throws InterruptedException { List outputScraper = Collections.synchronizedList(new ArrayList()); CountDownLatch countDownLatch = new CountDownLatch(5); List workers = Stream .generate(() -> new Thread(new Worker(outputScraper, countDownLatch))) .limit(5) .collect(toList()); workers.forEach(Thread::start); countDownLatch.await(); outputScraper.add("Latch released"); assertThat(outputScraper) .containsExactly( "Counted down", "Counted down", "Counted down", "Counted down", "Counted down", "Latch released" ); }

Secara semula jadi "Latch dilepaskan" akan selalu menjadi output terakhir - kerana bergantung pada pengeluaran CountDownLatch .

Perhatikan bahawa jika kita tidak memanggil tunggu () , kita tidak dapat menjamin urutan pelaksanaan utas, jadi ujian akan gagal secara rawak.

4. Kolam Benang Menunggu Permulaan

Sekiranya kita mengambil contoh sebelumnya, tetapi kali ini memulakan ribuan utas dan bukannya lima, kemungkinan banyak yang terdahulu akan selesai diproses sebelum kita memanggil permulaan () pada yang kemudian. Ini boleh menjadikannya sukar untuk mencuba dan menghasilkan semula masalah serentak, kerana kita tidak akan dapat menjalankan semua urutan kita secara selari.

Untuk menyelesaikannya, mari kita mengira CountdownLatch berfungsi berbeza daripada contoh sebelumnya. Daripada menyekat utas induk sehingga beberapa utas anak selesai, kami dapat menyekat setiap utas anak sehingga semua yang lain bermula.

Mari ubah kaedah run () kami sehingga menyekat sebelum memproses:

public class WaitingWorker implements Runnable { private List outputScraper; private CountDownLatch readyThreadCounter; private CountDownLatch callingThreadBlocker; private CountDownLatch completedThreadCounter; public WaitingWorker( List outputScraper, CountDownLatch readyThreadCounter, CountDownLatch callingThreadBlocker, CountDownLatch completedThreadCounter) { this.outputScraper = outputScraper; this.readyThreadCounter = readyThreadCounter; this.callingThreadBlocker = callingThreadBlocker; this.completedThreadCounter = completedThreadCounter; } @Override public void run() { readyThreadCounter.countDown(); try { callingThreadBlocker.await(); doSomeWork(); outputScraper.add("Counted down"); } catch (InterruptedException e) { e.printStackTrace(); } finally { completedThreadCounter.countDown(); } } }

Sekarang, mari ubah suai ujian kami sehingga ia menyekat sehingga semua Pekerja bermula, buka blok pekerja, dan kemudian blok sehingga Pekerja selesai:

@Test public void whenDoingLotsOfThreadsInParallel_thenStartThemAtTheSameTime() throws InterruptedException { List outputScraper = Collections.synchronizedList(new ArrayList()); CountDownLatch readyThreadCounter = new CountDownLatch(5); CountDownLatch callingThreadBlocker = new CountDownLatch(1); CountDownLatch completedThreadCounter = new CountDownLatch(5); List workers = Stream .generate(() -> new Thread(new WaitingWorker( outputScraper, readyThreadCounter, callingThreadBlocker, completedThreadCounter))) .limit(5) .collect(toList()); workers.forEach(Thread::start); readyThreadCounter.await(); outputScraper.add("Workers ready"); callingThreadBlocker.countDown(); completedThreadCounter.await(); outputScraper.add("Workers complete"); assertThat(outputScraper) .containsExactly( "Workers ready", "Counted down", "Counted down", "Counted down", "Counted down", "Counted down", "Workers complete" ); }

Corak ini sangat berguna untuk mencuba menghasilkan bug serentak, seperti yang dapat digunakan untuk memaksa ribuan utas untuk mencuba dan melakukan beberapa logik secara selari.

5. Menamatkan CountdownLatch Awal

Kadang-kadang, kita mungkin menghadapi situasi di mana Pekerja berhenti secara salah sebelum mengira CountDownLatch. Ini boleh mengakibatkan ia tidak pernah mencapai sifar dan menunggu () tidak pernah berhenti:

@Override public void run() { if (true) { throw new RuntimeException("Oh dear, I'm a BrokenWorker"); } countDownLatch.countDown(); outputScraper.add("Counted down"); }

Mari ubah ujian sebelumnya untuk menggunakan BrokenWorker, untuk menunjukkan bagaimana menunggu () akan menyekat selamanya:

@Test public void whenFailingToParallelProcess_thenMainThreadShouldGetNotGetStuck() throws InterruptedException { List outputScraper = Collections.synchronizedList(new ArrayList()); CountDownLatch countDownLatch = new CountDownLatch(5); List workers = Stream .generate(() -> new Thread(new BrokenWorker(outputScraper, countDownLatch))) .limit(5) .collect(toList()); workers.forEach(Thread::start); countDownLatch.await(); }

Jelas, ini bukan tingkah laku yang kita mahukan - lebih baik aplikasi diteruskan daripada sekatan tanpa batas.

Untuk mengatasi perkara ini, mari tambahkan argumen tamat pada panggilan kami untuk menunggu ().

boolean completed = countDownLatch.await(3L, TimeUnit.SECONDS); assertThat(completed).isFalse();

Seperti yang kita lihat, ujian akhirnya akan habis dan menunggu () akan kembali palsu .

6. Kesimpulannya

Dalam panduan ringkas ini, kami telah menunjukkan bagaimana kami dapat menggunakan CountDownLatch untuk menyekat utas sehingga utas lain selesai beberapa proses.

Kami juga telah menunjukkan bagaimana ia boleh digunakan untuk membantu menyelesaikan masalah serentak dengan memastikan urutan berjalan selari.

Pelaksanaan contoh-contoh ini boleh didapati di GitHub; ini adalah projek berasaskan Maven, jadi seharusnya mudah dijalankan seperti sedia kala.