Threading modern: Primer konkurensi Java

Banyak hal yang perlu dipelajari tentang pemrograman dengan utas Java tidak berubah secara dramatis selama evolusi platform Java, tetapi telah berubah secara bertahap. Dalam primer utas Java ini, Cameron Laird mencapai beberapa titik utas tinggi (dan rendah) sebagai teknik pemrograman bersamaan. Dapatkan gambaran umum tentang apa yang selalu menantang tentang pemrograman multithread dan cari tahu bagaimana platform Java telah berevolusi untuk memenuhi beberapa tantangan.

Concurrency adalah salah satu kekhawatiran terbesar bagi pendatang baru di pemrograman Java tetapi tidak ada alasan untuk membiarkannya membuat Anda takut. Tidak hanya dokumentasi yang sangat baik tersedia (kami akan menjelajahi beberapa sumber di artikel ini) tetapi utas Java menjadi lebih mudah untuk digunakan seiring dengan berkembangnya platform Java. Untuk mempelajari cara melakukan pemrograman multithread di Java 6 dan 7, Anda benar-benar hanya memerlukan beberapa blok penyusun. Kami akan mulai dengan ini:

  • Program berulir sederhana
  • Threading adalah tentang kecepatan, bukan?
  • Tantangan konkurensi Java
  • Kapan menggunakan Runnable
  • Ketika benang bagus menjadi buruk
  • Apa yang baru di Java 6 dan 7
  • Apa selanjutnya untuk utas Java

Artikel ini adalah survei pemula tentang teknik threading Java, termasuk tautan ke beberapa artikel pengantar JavaWorld yang paling sering dibaca tentang pemrograman multithread. Jalankan mesin Anda dan ikuti tautan di atas jika Anda siap untuk mulai belajar tentang Java threading hari ini.

Program berulir sederhana

Pertimbangkan sumber Java berikut.

Kode 1. FirstThreadingExample

class FirstThreadingExample { public static void main (String [] args) { // The second argument is a delay between // successive outputs. The delay is // measured in milliseconds. "10", for // instance, means, "print a line every // hundredth of a second". ExampleThread mt = new ExampleThread("A", 31); ExampleThread mt2 = new ExampleThread("B", 25); ExampleThread mt3 = new ExampleThread("C", 10); mt.start(); mt2.start(); mt3.start(); } } class ExampleThread extends Thread { private int delay; public ExampleThread(String label, int d) { // Give this particular thread a // name: "thread 'LABEL'". super("thread '" + label + "'"); delay = d; } public void run () { for (int count = 1, row = 1; row < 20; row++, count++) { try { System.out.format("Line #%d from %s\n", count, getName()); Thread.currentThread().sleep(delay); } catch (InterruptedException ie) { // This would be a surprise. } } } }

Sekarang kompilasi dan jalankan sumber ini seperti yang Anda lakukan pada aplikasi baris perintah Java lainnya. Anda akan melihat keluaran yang terlihat seperti ini:

Kode 2. Output dari program berulir

Line #1 from thread 'A' Line #1 from thread 'C' Line #1 from thread 'B' Line #2 from thread 'C' Line #3 from thread 'C' Line #2 from thread 'B' Line #4 from thread 'C' ... Line #17 from thread 'B' Line #14 from thread 'A' Line #18 from thread 'B' Line #15 from thread 'A' Line #19 from thread 'B' Line #16 from thread 'A' Line #17 from thread 'A' Line #18 from thread 'A' Line #19 from thread 'A'

Itu dia - Anda seorang Threadprogrammer Java !

Oke, mungkin tidak terlalu cepat. Sekecil program dalam Daftar 1, program ini berisi beberapa kehalusan yang patut kita perhatikan.

Benang dan ketidakpastian

Siklus belajar yang khas dengan pemrograman terdiri dari empat tahap: (1) Mempelajari konsep baru; (2) menjalankan program contoh; (3) membandingkan output dengan ekspektasi; dan (4) mengulang sampai keduanya cocok. Perhatikan, bahwa sebelumnya saya mengatakan output untuk FirstThreadingExampleakan terlihat "seperti" Listing 2. Jadi, itu berarti output Anda bisa berbeda dari milik saya, baris demi baris. Tentang apa itu ?

Dalam program Java yang paling sederhana, ada jaminan urutan eksekusi: baris pertama main()akan dieksekusi terlebih dahulu, lalu baris berikutnya, dan seterusnya, dengan penelusuran yang sesuai masuk dan keluar dari metode lain. Threadmelemahkan jaminan itu.

Threading menghadirkan kekuatan baru untuk pemrograman Java; Anda dapat mencapai hasil dengan utas yang tidak dapat Anda lakukan tanpanya. Tetapi kekuatan itu datang dengan mengorbankan determinasi . Dalam program Java yang paling sederhana, ada jaminan urutan eksekusi: baris pertama main()akan dieksekusi terlebih dahulu, lalu baris berikutnya, dan seterusnya, dengan penelusuran yang sesuai masuk dan keluar dari metode lain. Threadmelemahkan jaminan itu. Dalam program multithread, " Line #17 from thread B" mungkin muncul di layar Anda sebelum atau sesudah " Line #14 from thread A," dan urutannya mungkin berbeda pada eksekusi program yang sama secara berurutan, bahkan di komputer yang sama.

Ketidakpastian mungkin asing, tetapi tidak perlu mengganggu. Urutan eksekusi dalam utas tetap dapat diprediksi, dan ada juga keuntungan yang terkait dengan ketidakpastian. Anda mungkin pernah mengalami hal serupa saat bekerja dengan antarmuka pengguna grafis (GUI). Pemroses acara di Swing atau penangan acara di HTML adalah contohnya.

Meskipun diskusi lengkap tentang sinkronisasi utas berada di luar cakupan pendahuluan ini, mudah untuk menjelaskan dasar-dasarnya.

Misalnya, pertimbangkan mekanisme bagaimana HTML menentukan ... onclick = "myFunction();" ...untuk menentukan tindakan yang akan terjadi setelah pengguna mengklik. Kasus ketidakpastian yang sudah dikenal ini menggambarkan beberapa keuntungannya. Dalam kasus ini, myFunction()tidak dijalankan pada waktu tertentu sehubungan dengan elemen kode sumber lainnya, tetapi dalam kaitannya dengan tindakan pengguna akhir . Jadi ketidakpastian bukan hanya kelemahan dalam sistem; ini juga merupakan pengayaan model eksekusi, yang memberikan kesempatan baru bagi programmer untuk menentukan urutan dan ketergantungan.

Penundaan eksekusi dan subclass Thread

Anda dapat belajar FirstThreadingExampledengan bereksperimen sendiri. Coba tambahkan atau hapus ExampleThreads - yaitu, pemanggilan konstruktor seperti ... new ExampleThread(label, delay);- dan mengutak-atik delays. Ide dasarnya adalah bahwa program memulai tiga bagian yang terpisah Thread, yang kemudian berjalan sendiri-sendiri hingga selesai. Untuk membuat eksekusinya lebih instruktif, masing-masing penundaan sedikit antara baris berurutan yang ditulisnya ke keluaran; ini memberikan benang lain kesempatan untuk menulis mereka output.

Perhatikan bahwa Threadpemrograman berbasis tidak, secara umum, memerlukan penanganan InterruptedException. Yang ditampilkan di FirstThreadingExampleberhubungan dengan sleep(), daripada berhubungan langsung dengan Thread. ThreadSumber berbasis paling banyak tidak mencakup a sleep(); tujuan dari sleep()sini adalah untuk memodelkan, dengan cara yang sederhana, perilaku metode yang berjalan lama yang ditemukan "di alam liar".

Sesuatu yang lain untuk pemberitahuan pada Listing 1 adalah bahwa Threadadalah abstrak kelas, dirancang untuk subclassed. run()Metode defaultnya tidak melakukan apa-apa, jadi harus diganti dalam definisi subclass untuk mencapai sesuatu yang berguna.

Ini semua tentang kecepatan, bukan?

Jadi sekarang Anda dapat melihat sedikit tentang apa yang membuat pemrograman dengan utas menjadi rumit. Tetapi poin utama dari menanggung semua kesulitan ini bukanlah untuk mendapatkan kecepatan.

Program multithread tidak , secara umum, selesai lebih cepat daripada program single-threaded - bahkan program ini bisa jauh lebih lambat dalam kasus patologis. Nilai tambah mendasar dari program multithread adalah responsivitas . Ketika beberapa inti pemrosesan tersedia untuk JVM, atau ketika program menghabiskan banyak waktu menunggu pada beberapa sumber daya eksternal seperti respons jaringan, maka multithreading dapat membantu program selesai lebih cepat.

Pikirkan aplikasi GUI: jika masih menanggapi poin dan klik pengguna akhir saat mencari sidik jari yang cocok "di latar belakang" atau menghitung ulang kalender untuk turnamen tenis tahun depan, maka itu dibangun dengan mempertimbangkan konkurensi. Arsitektur aplikasi serentak yang khas menempatkan pengenalan dan respons terhadap tindakan pengguna di thread yang terpisah dari thread komputasi yang ditetapkan untuk menangani beban back-end yang besar. (Lihat "Penguliran ayun dan alur pengiriman peristiwa" untuk ilustrasi lebih lanjut dari prinsip-prinsip ini.)

Dalam pemrograman Anda sendiri, Anda kemungkinan besar akan mempertimbangkan untuk menggunakan Threads dalam salah satu situasi berikut:

  1. Aplikasi yang sudah ada memiliki fungsi yang benar tetapi terkadang tidak responsif. "Pemblokiran" ini sering kali berkaitan dengan sumber daya eksternal di luar kendali Anda: kueri database yang memakan waktu, penghitungan yang rumit, pemutaran multimedia, atau respons jaringan dengan latensi yang tidak terkendali.
  2. Aplikasi yang intensif secara komputasi dapat memanfaatkan host multicore dengan lebih baik. Ini mungkin kasus seseorang yang merender grafik kompleks atau mensimulasikan model ilmiah yang terlibat.
  3. Threadsecara alami mengekspresikan model pemrograman yang dibutuhkan aplikasi. Misalnya, Anda mencontohkan perilaku pengemudi mobil pada jam sibuk atau lebah di dalam sarang. Untuk mengimplementasikan setiap driver atau lebah sebagai Threadobjek terkait mungkin nyaman dari sudut pandang pemrograman, terlepas dari pertimbangan kecepatan atau daya tanggap.

Tantangan konkurensi Java

Programmer berpengalaman Ned Batchelder baru-baru ini menyindir

Beberapa orang, ketika dihadapkan pada suatu masalah, berpikir, "Saya tahu, saya akan menggunakan utas," dan kemudian dua mereka memiliki erpoblesms.

Itu lucu karena sangat baik memodelkan masalah dengan konkurensi. Seperti yang telah saya sebutkan, program multithread cenderung memberikan hasil yang berbeda dalam hal urutan atau waktu eksekusi utas yang tepat. Itu mengganggu pemrogram, yang dilatih untuk berpikir dalam kaitannya dengan hasil yang dapat direproduksi, penentuan ketat, dan urutan invarian.

Lebih buruk. Untaian yang berbeda mungkin tidak hanya menghasilkan hasil dalam urutan yang berbeda, tetapi juga dapat bersaing di tingkat yang lebih penting untuk hasil. Sangat mudah bagi pendatang baru untuk melakukan multithreading ke close()pegangan file dalam satu pegangan Threadsebelum yang lain Threadmenyelesaikan semua yang dibutuhkan untuk menulis.

Menguji program bersamaan

Sepuluh tahun yang lalu di JavaWorld, Dave Dyer mencatat bahwa bahasa Java memiliki satu fitur yang "digunakan secara tidak benar" sehingga ia menggolongkannya sebagai cacat desain yang serius. Fitur itu multithreading.

Komentar Dyer menyoroti tantangan pengujian program multithread. Ketika Anda tidak dapat lagi dengan mudah menentukan keluaran dari program dalam hal urutan karakter tertentu, akan ada dampak pada seberapa efektif Anda dapat menguji kode berulir Anda.

The correct starting point to resolving the intrinsic difficulties of concurrent programming was well stated by Heinz Kabutz in his Java Specialist newsletter: recognize that concurrency is a topic that you should understand and study it systematically. There are of course tools such as diagramming techniques and formal languages that will help. But the first step is to sharpen your intuition by practicing with simple programs like FirstThreadingExample in Listing 1. Next, learn as much as you can about threading fundamentals like these:

  • Synchronization and immutable objects
  • Thread scheduling and wait/notify
  • Race conditions and deadlock
  • Thread monitors for exclusive access, conditions, and assertions
  • JUnit best practices -- testing multithreaded code

When to use Runnable

Object orientation in Java defines singly inherited classes, which has consequences for multithreading coding. To this point, I have only described a use for Thread that was based on subclasses with an overridden run(). In an object design that already involved inheritance, this simply wouldn't work. You cannot simultaneously inherit from RenderedObject or ProductionLine or MessageQueue alongside Thread!

This constraint affects many areas of Java, not just multithreading. Fortunately, there's a classical solution for the problem, in the form of the Runnable interface. As explained by Jeff Friesen in his 2002 introduction to threading, the Runnable interface is made for situations where subclassing Thread isn't possible:

The Runnableantarmuka menyatakan metode tanda tangan tunggal: void run();. Tanda tangan yang identik dengan Thread's run()metode tanda tangan dan berfungsi sebagai entri thread eksekusi. Karena Runnablemerupakan antarmuka, setiap kelas dapat mengimplementasikan antarmuka itu dengan melampirkan implementsklausa ke tajuk kelas dan dengan menyediakan run()metode yang sesuai . Pada waktu eksekusi, kode program dapat membuat objek, atau runnable , dari kelas itu dan meneruskan referensi runnable ke Threadkonstruktor yang sesuai .

Jadi untuk kelas yang tidak bisa diperluas Thread, Anda harus membuat runnable untuk memanfaatkan multithreading. Secara semantik, jika Anda melakukan pemrograman tingkat sistem dan kelas Anda berada dalam relasi is-a Thread, maka Anda harus membuat subkelas langsung dari Thread. Tetapi sebagian besar penggunaan multithreading pada tingkat aplikasi bergantung pada komposisi, dan dengan demikian menentukan Runnablekompatibilitas dengan diagram kelas aplikasi. Untungnya, hanya dibutuhkan satu atau dua baris tambahan untuk membuat kode menggunakan Runnableantarmuka, seperti yang ditunjukkan pada Daftar 3 di bawah ini.