Java 101: Memahami benang Java, Bahagian 1: Memperkenalkan benang dan runnables

Artikel ini adalah yang pertama dalam siri Java 101 empat bahagian yang meneroka benang Java. Walaupun anda mungkin menganggap threading di Jawa sangat sukar untuk dipahami, saya ingin menunjukkan kepada anda bahawa benang mudah difahami. Dalam artikel ini, saya memperkenalkan anda pada thread Java dan runnables. Dalam artikel seterusnya, kami akan meneroka penyegerakan (melalui kunci), masalah penyegerakan (seperti kebuntuan), mekanisme tunggu / maklumkan, penjadualan (dengan dan tanpa keutamaan), gangguan benang, pemasa, turun naik, kumpulan utas, dan benang pemboleh ubah tempatan .

Perhatikan bahawa artikel ini (sebahagian daripada arkib JavaWorld) telah dikemas kini dengan senarai kod baru dan kod sumber yang boleh dimuat turun pada bulan Mei 2013.

Memahami benang Java - baca keseluruhan siri

  • Bahagian 1: Memperkenalkan benang dan runnables
  • Bahagian 2: Penyegerakan
  • Bahagian 3: Penjadualan benang dan tunggu / maklumkan
  • Bahagian 4: Kumpulan benang dan turun naik

Apa itu utas?

Secara konseptual, konsep benang tidak sukar untuk dipahami: ini adalah jalan pelaksanaan yang bebas melalui kod program. Apabila banyak utas dijalankan, jalan satu utas melalui kod yang sama biasanya berbeza dari yang lain. Sebagai contoh, anggap satu utas melaksanakan kod bait yang setara dengan bahagian pernyataan if-else if, sementara utas lain melaksanakan kod bait yang setara dengan elsebahagian tersebut. Bagaimana JVM mengawasi pelaksanaan setiap utas? JVM memberikan setiap thread kaedah panggilan-panggilannya sendiri. Sebagai tambahan untuk melacak arahan kod byte semasa, tumpukan kaedah-kaedah mengesan pemboleh ubah tempatan, parameter yang diteruskan JVM ke suatu metode, dan nilai pengembalian metode.

Apabila beberapa utas menjalankan urutan arahan kod byte dalam program yang sama, tindakan itu dikenali sebagai multithreading . Multithreading memberi manfaat kepada program dengan pelbagai cara:

  • Program berasaskan GUI multithreaded (antara muka pengguna grafik) tetap responsif kepada pengguna ketika melakukan tugas lain, seperti melakukan repaginasi atau mencetak dokumen.
  • Program berulir biasanya selesai lebih cepat daripada rakan mereka yang tidak diulirkan. Ini terutama berlaku untuk benang yang berjalan pada mesin multiprosesor, di mana setiap utas mempunyai pemprosesnya sendiri.

Java mencapai multithreading melalui java.lang.Threadkelasnya. Setiap Threadobjek menerangkan satu utas pelaksanaan. Pelaksanaan yang berlaku dalam Thread's run()kaedah. Kerana run()kaedah lalai tidak melakukan apa-apa, anda mesti menundukkan kelas Threaddan mengatasi run()untuk menyelesaikan kerja yang bermanfaat. Untuk rasa benang dan multithreading dalam konteks Thread, periksa Penyenaraian 1:

Penyenaraian 1. ThreadDemo.java

// ThreadDemo.java class ThreadDemo { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); for (int i = 0; i < 50; i++) System.out.println ("i = " + i + ", i * i = " + i * i); } } class MyThread extends Thread { public void run () { for (int count = 1, row = 1; row < 20; row++, count++) { for (int i = 0; i < count; i++) System.out.print ('*'); System.out.print ('\n'); } } }

Penyenaraian 1 menunjukkan kod sumber untuk aplikasi yang terdiri daripada kelas ThreadDemodan MyThread. Kelas ThreadDemomendorong aplikasi dengan membuat MyThreadobjek, memulakan utas yang berkait dengan objek itu, dan melaksanakan beberapa kod untuk mencetak jadual kotak. Sebaliknya, MyThreadmengatasi Thread's run()kaedah untuk mencetak (atas aliran output standard) sebuah segitiga hak-sudut terdiri daripada aksara asterisk.

Penjadualan benang dan JVM

Sebilangan besar (jika tidak semua) pelaksanaan JVM menggunakan keupayaan threading platform yang mendasari. Kerana kemampuan tersebut khusus platform, urutan output program multithread anda mungkin berbeza dari urutan output orang lain. Perbezaan itu berpunca daripada penjadualan, topik yang saya terokai kemudian dalam siri ini.

Apabila anda menaip java ThreadDemountuk menjalankan aplikasi, JVM membuat permulaan pelaksanaan, yang melaksanakan main()kaedah tersebut. Dengan melaksanakan mt.start ();, utas permulaan memberitahu JVM untuk membuat urutan pelaksanaan kedua yang melaksanakan arahan kod bait yang merangkumi kaedah MyThreadobjek run(). Apabila start()kaedah kembali, utas permulaan menjalankan forgelungnya untuk mencetak meja kotak, sementara utas baru melaksanakan run()kaedah untuk mencetak segitiga sudut kanan.

Seperti apa outputnya? Lari ThreadDemountuk mengetahui. Anda akan melihat output setiap benang cenderung bersilang dengan output yang lain. Itu hasil kerana kedua-dua utas menghantar outputnya ke aliran output standard yang sama.

Kelas Thread

Untuk menjadi mahir dalam menulis kod multithread, anda mesti terlebih dahulu memahami pelbagai kaedah yang membentuk Threadkelas. Bahagian ini menerangkan banyak kaedah tersebut. Secara khusus, anda belajar mengenai kaedah untuk memulakan utas, menamakan benang, meletakkan benang untuk tidur, menentukan sama ada benang masih hidup, menggabungkan satu utas ke utas yang lain, dan menghitung semua utas aktif dalam kumpulan dan subkumpulan utas semasa. Saya juga membincangkan Threadalat bantu debug dan utas pengguna berbanding benang daemon.

Saya akan membentangkan sisa Threadkaedah dalam artikel berikutnya, kecuali kaedah Sun yang tidak digunakan lagi.

Kaedah yang tidak digunakan lagi

Sun telah menghentikan penggunaan berbagai Threadmetode, seperti suspend()dan resume(), kerana mereka dapat mengunci program anda atau merosakkan objek. Akibatnya, anda tidak boleh memanggilnya dalam kod anda. Rujuk dokumentasi SDK untuk penyelesaian kaedah tersebut. Saya tidak merangkumi kaedah yang tidak digunakan dalam siri ini.

Membina benang

Threadmempunyai lapan pembina. Yang paling mudah adalah:

  • Thread(), yang membuat Threadobjek dengan nama lalai
  • Thread(String name), yang membuat Threadobjek dengan nama yang nameditentukan oleh argumen

Pembina termudah seterusnya ialah Thread(Runnable target)dan Thread(Runnable target, String name). Terlepas dari Runnableparameternya, konstruktor tersebut sama dengan pembina yang disebutkan di atas. Perbezaannya: RunnableParameter mengenal pasti objek di luar Threadyang menyediakan run()kaedah. (Anda belajar tentang Runnablekemudian dalam artikel ini.) Final empat pengeluar menyerupai Thread(String name), Thread(Runnable target)dan Thread(Runnable target, String name); namun, pembina akhir juga menyertakan ThreadGrouphujah untuk tujuan organisasi.

Salah satu daripada empat pembina terakhir Thread(ThreadGroup group, Runnable target, String name, long stackSize), menarik kerana ia membolehkan anda menentukan ukuran yang dikehendaki dari timbunan kaedah panggilan. Mampu menentukan ukuran itu terbukti bermanfaat dalam program dengan kaedah yang menggunakan rekursi - teknik pelaksanaan di mana kaedah berulang kali memanggil dirinya sendiri - untuk menyelesaikan masalah tertentu dengan elegan. Dengan menetapkan ukuran timbunan secara eksplisit, kadangkala anda dapat mencegah StackOverflowErrors. Namun, ukuran yang terlalu besar boleh mengakibatkan OutOfMemoryErrors. Juga, Sun menganggap ukuran timbunan panggilan kaedah bergantung pada platform. Bergantung pada platform, ukuran tumpukan kaedah panggilan mungkin berubah. Oleh itu, fikirkan dengan teliti mengenai kesan ke atas program anda sebelum menulis kod yang memanggil Thread(ThreadGroup group, Runnable target, String name, long stackSize).

Mulakan kenderaan anda

Benang menyerupai kenderaan: mereka memindahkan program dari awal hingga akhir. Threaddan Threadobjek subkelas bukan utas. Sebaliknya, mereka menerangkan atribut utas, seperti namanya, dan mengandungi kod (melalui run()kaedah) yang dijalankan oleh utas. Apabila tiba masanya utas baru dijalankan run(), utas lain memanggil kaedah Threadobjek subclass start(). Sebagai contoh, untuk memulakan main()utas kedua, utas permulaan aplikasi — yang melaksanakan — panggilan start(). Sebagai tindak balas, kod pengendalian benang JVM berfungsi dengan platform untuk memastikan utas memulakan dan memanggil kaedah Threadobjek subclass dengan betul run().

Setelah start()selesai, pelbagai utas dilaksanakan. Oleh kerana kita cenderung berfikir secara linear, kita sering merasa sukar untuk memahami aktiviti serentak (serentak) yang berlaku semasa dua atau lebih utas berjalan. Oleh itu, anda harus memeriksa carta yang menunjukkan di mana utas menjalankan (kedudukannya) berbanding masa. Rajah di bawah menunjukkan carta sedemikian.

Carta menunjukkan beberapa jangka masa yang signifikan:

  • Permulaan benang permulaan
  • Momen benang mula dilaksanakan main()
  • Momen benang mula dilaksanakan start()
  • Momen itu start()mencipta utas baru dan kembali kemain()
  • Permulaan thread baru
  • Sebaik sahaja thread baru mula dilaksanakan run()
  • Momen berbeza setiap thread berakhir

Perhatikan bahawa inisialisasi thread baru, pelaksanaannya run(), dan penamatannya berlaku serentak dengan pelaksanaan thread permulaan. Juga perhatikan bahawa setelah panggilan utas start(), panggilan berikutnya ke kaedah itu sebelum run()kaedah keluar menyebabkan start()melempar java.lang.IllegalThreadStateExceptionobjek.

Apa namanya?

During a debugging session, distinguishing one thread from another in a user-friendly fashion proves helpful. To differentiate among threads, Java associates a name with a thread. That name defaults to Thread, a hyphen character, and a zero-based integer number. You can accept Java's default thread names or you can choose your own. To accommodate custom names, Thread provides constructors that take name arguments and a setName(String name) method. Thread also provides a getName() method that returns the current name. Listing 2 demonstrates how to establish a custom name via the Thread(String name) constructor and retrieve the current name in the run() method by calling getName():

Listing 2. NameThatThread.java

// NameThatThread.java class NameThatThread { public static void main (String [] args) { MyThread mt; if (args.length == 0) mt = new MyThread (); else mt = new MyThread (args [0]); mt.start (); } } class MyThread extends Thread { MyThread () { // The compiler creates the byte code equivalent of super (); } MyThread (String name) { super (name); // Pass name to Thread superclass } public void run () { System.out.println ("My name is: " + getName ()); } }

You can pass an optional name argument to MyThread on the command line. For example, java NameThatThread X establishes X as the thread's name. If you fail to specify a name, you'll see the following output:

My name is: Thread-1

If you prefer, you can change the super (name); call in the MyThread (String name) constructor to a call to setName (String name)—as in setName (name);. That latter method call achieves the same objective—establishing the thread's name—as super (name);. I leave that as an exercise for you.

Naming main

Java assigns the name main to the thread that runs the main() method, the starting thread. You typically see that name in the Exception in thread "main" message that the JVM's default exception handler prints when the starting thread throws an exception object.

To sleep or not to sleep

Later in this column, I will introduce you to animation— repeatedly drawing on one surface images that slightly differ from each other to achieve a movement illusion. To accomplish animation, a thread must pause during its display of two consecutive images. Calling Thread's static sleep(long millis) method forces a thread to pause for millis milliseconds. Another thread could possibly interrupt the sleeping thread. If that happens, the sleeping thread awakes and throws an InterruptedException object from the sleep(long millis) method. As a result, code that calls sleep(long millis) must appear within a try block—or the code's method must include InterruptedException in its throws clause.

Untuk menunjukkan sleep(long millis), saya telah menulis CalcPI1permohonan. Aplikasi itu memulakan utas baru yang menggunakan algoritma matematik untuk mengira nilai pi pemalar matematik. Semasa thread baru mengira, utas permulaan berhenti selama 10 milisaat dengan memanggil sleep(long millis). Setelah benang permulaan terjaga, ia mencetak nilai pi, yang benang baru simpan dalam pemboleh ubah pi. Penyenaraian 3 menyajikan CalcPI1kod sumber:

Penyenaraian 3. CalcPI1.java

// CalcPI1.java class CalcPI1 { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); try { Thread.sleep (10); // Sleep for 10 milliseconds } catch (InterruptedException e) { } System.out.println ("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; // Initializes to 0.0, by default public void run () { for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println ("Finished calculating PI"); } }

Sekiranya anda menjalankan program ini, anda akan melihat output yang serupa (tetapi mungkin tidak serupa) dengan yang berikut:

pi = -0.2146197014017295 Finished calculating PI