Tingkah laku benang dalam JVM

Threading merujuk kepada praktik melaksanakan proses pengaturcaraan secara serentak untuk meningkatkan prestasi aplikasi. Walaupun tidak biasa bekerja dengan utas secara langsung dalam aplikasi perniagaan, mereka selalu digunakan dalam kerangka kerja Java.

Sebagai contoh, kerangka kerja yang memproses sejumlah besar maklumat, seperti Spring Batch, menggunakan utas untuk menguruskan data. Memanipulasi urutan atau proses CPU secara bersamaan meningkatkan prestasi, menghasilkan program yang lebih pantas dan lebih efisien.

Dapatkan kod sumber

Dapatkan kod untuk Java Challenger ini. Anda boleh menjalankan ujian anda sendiri semasa anda mengikuti contohnya.

Cari utas pertama anda: kaedah utama () Java

Walaupun anda tidak pernah bekerja secara langsung dengan benang Java, anda telah bekerja secara tidak langsung dengan mereka kerana kaedah utama () Java mengandungi Thread utama. Bila-bila masa anda melaksanakan main()kaedah tersebut, anda juga telah menjalankan kaedah utama Thread.

Mempelajari Threadkelas sangat membantu untuk memahami bagaimana threading berfungsi dalam program Java. Kita dapat mengakses utas yang sedang dijalankan dengan menggunakan currentThread().getName()metode, seperti yang ditunjukkan di sini:

 public class MainThread { public static void main(String... mainThread) { System.out.println(Thread.currentThread().getName()); } } 

Kod ini akan mencetak "utama", mengenal pasti utas yang sedang dijalankan. Mengetahui bagaimana mengenal pasti benang yang dilaksanakan adalah langkah pertama untuk menyerap konsep benang.

Kitaran hidup benang Java

Semasa bekerja dengan utas, sangat penting untuk mengetahui keadaan benang. Kitaran hidup benang Java terdiri daripada enam keadaan utas:

  • Baru : Yang baru Thread()telah dibuat.
  • Runnable : The Thread's start()kaedah telah digunakan.
  • Menjalankan : start()Kaedah telah dipanggil dan utas sedang berjalan.
  • Digantung : Benang digantung buat sementara waktu, dan dapat disambung semula oleh utas lain.
  • Disekat : Benang sedang menunggu peluang untuk dijalankan. Ini berlaku apabila satu utas telah menggunakan synchronized()kaedah dan utas seterusnya mesti menunggu sehingga selesai.
  • Ditamatkan : Pelaksanaan utas selesai.
Rafael Chinelato Del Nero

Masih banyak lagi yang boleh diterokai dan difahami mengenai keadaan utas, tetapi maklumat dalam Rajah 1 sudah cukup untuk anda menyelesaikan cabaran Java ini.

Pemprosesan serentak: Memperluas kelas Thread

Pada proses yang paling sederhana, serentak dilakukan dengan memperluas Threadkelas, seperti yang ditunjukkan di bawah.

 public class InheritingThread extends Thread { InheritingThread(String threadName) { super(threadName); } public static void main(String... inheriting) { System.out.println(Thread.currentThread().getName() + " is running"); new InheritingThread("inheritingThread").start(); } @Override public void run() { System.out.println(Thread.currentThread().getName() + " is running"); } } 

Di sini kita menjalankan dua utas: the MainThreadand the InheritingThread. Apabila kita menggunakan start()kaedah dengan yang baru inheritingThread(), logik dalam run()kaedah dijalankan.

Kami juga memberikan nama benang kedua dalam Threadkonstruktor kelas, jadi outputnya adalah:

 main is running. inheritingThread is running. 

Antara muka Runnable

Daripada menggunakan warisan, anda boleh melaksanakan antara muka Runnable. Lulus Runnabledi dalam Threadkeputusan pembina dalam gandingan kurang dan lebih fleksibiliti. Setelah lulus Runnable, kita boleh menggunakan start()kaedah seperti yang kita lakukan pada contoh sebelumnya:

 public class RunnableThread implements Runnable { public static void main(String... runnableThread) { System.out.println(Thread.currentThread().getName()); new Thread(new RunnableThread()).start(); } @Override public void run() { System.out.println(Thread.currentThread().getName()); } } 

Benang bukan daemon vs daemon

Dari segi pelaksanaan, terdapat dua jenis utas:

  • Benang bukan daemon dijalankan hingga akhir. Benang utama adalah contoh yang baik dari benang bukan daemon. Kod masuk main()akan selalu dilaksanakan hingga akhir, melainkan jika System.exit()program itu diselesaikan.
  • A thread daemon adalah sebaliknya, pada dasarnya satu proses yang tidak perlu dilaksanakan sehingga akhir.

Ingat peraturannya : Jika benang bukan daemon yang dilampirkan berakhir sebelum utas daemon, utas daemon tidak akan dijalankan hingga akhir.

Untuk lebih memahami hubungan benang daemon dan bukan daemon, pelajari contoh ini:

 import java.util.stream.IntStream; public class NonDaemonAndDaemonThread { public static void main(String... nonDaemonAndDaemon) throws InterruptedException { System.out.println("Starting the execution in the Thread " + Thread.currentThread().getName()); Thread daemonThread = new Thread(() -> IntStream.rangeClosed(1, 100000) .forEach(System.out::println)); daemonThread.setDaemon(true); daemonThread.start(); Thread.sleep(10); System.out.println("End of the execution in the Thread " + Thread.currentThread().getName()); } } 

Dalam contoh ini saya telah menggunakan benang daemon untuk menyatakan julat dari 1 hingga 100,000, mengulang semuanya, dan kemudian mencetak. Tetapi ingat, benang daemon tidak akan menyelesaikan pelaksanaan jika utas utama bukan daemon selesai terlebih dahulu.

Hasilnya akan berjalan seperti berikut:

  1. Permulaan pelaksanaan di utas utama.
  2. Cetak nombor dari 1 hingga 100,000.
  3. Akhir pelaksanaan di utas utama, kemungkinan sebelum lelaran hingga 100,000 selesai.

Hasil akhir akan bergantung pada pelaksanaan JVM anda.

Dan itu membawa saya ke titik seterusnya: benang tidak dapat diramalkan.

Keutamaan utas dan JVM

Ada kemungkinan untuk memprioritaskan pelaksanaan thread dengan setPrioritykaedahnya, tetapi cara penanganannya bergantung pada pelaksanaan JVM. Linux, MacOS, dan Windows semuanya mempunyai implementasi JVM yang berbeza, dan masing-masing akan menangani keutamaan utas mengikut lalai sendiri.

Walau bagaimanapun, keutamaan utas yang anda tetapkan mempengaruhi urutan pemesejan utas. Tiga pemalar yang dinyatakan dalam Threadkelas adalah:

 /** * The minimum priority that a thread can have. */ public static final int MIN_PRIORITY = 1; /** * The default priority that is assigned to a thread. */ public static final int NORM_PRIORITY = 5; /** * The maximum priority that a thread can have. */ public static final int MAX_PRIORITY = 10; 

Try running some tests on the following code to see what execution priority you end up with:

 public class ThreadPriority { public static void main(String... threadPriority) { Thread moeThread = new Thread(() -> System.out.println("Moe")); Thread barneyThread = new Thread(() -> System.out.println("Barney")); Thread homerThread = new Thread(() -> System.out.println("Homer")); moeThread.setPriority(Thread.MAX_PRIORITY); barneyThread.setPriority(Thread.NORM_PRIORITY); homerThread.setPriority(Thread.MIN_PRIORITY); homerThread.start(); barneyThread.start(); moeThread.start(); } } 

Even if we set moeThread as MAX_PRIORITY, we cannot count on this thread being executed first. Instead, the order of execution will be random.

Constants vs enums

The Thread class was introduced with Java 1.0. At that time, priorities were set using constants, not enums. There's a problem with using constants, however: if we pass a priority number that is not in the range of 1 to 10, the setPriority() method will throw an IllegalArgumentException. Today, we can use enums to get around this issue. Using enums makes it impossible to pass an illegal argument, which both simplifies the code and gives us more control over its execution.

Take the Java threads challenge!

You've learned just a little bit about threads, but it's enough for this post's Java challenge.

To start, study the following code:

 public class ThreadChallenge { private static int wolverineAdrenaline = 10; public static void main(String... doYourBest) { new Motorcycle("Harley Davidson").start(); Motorcycle fastBike = new Motorcycle("Dodge Tomahawk"); fastBike.setPriority(Thread.MAX_PRIORITY); fastBike.setDaemon(false); fastBike.start(); Motorcycle yamaha = new Motorcycle("Yamaha YZF"); yamaha.setPriority(Thread.MIN_PRIORITY); yamaha.start(); } static class Motorcycle extends Thread { Motorcycle(String bikeName) { super(bikeName); } @Override public void run() { wolverineAdrenaline++; if (wolverineAdrenaline == 13) { System.out.println(this.getName()); } } } } 

What will be the output of this code? Analyze the code and try to determine the answer for yourself, based on what you've learned.

A. Harley Davidson

B. Dodge Tomahawk

C. Yamaha YZF

D. Indeterminate

What just happened? Understanding threads behavior

In the above code, we created three threads. The first thread is Harley Davidson, and we assigned this thread the default priority. The second thread is Dodge Tomahawk, assigned MAX_PRIORITY. The third is Yamaha YZF, with MIN_PRIORITY. Then we started the threads.

In order to determine the order the threads will run in, you might first note that the Motorcycle class extends the Thread class, and that we've passed the thread name in the constructor. We've also overridden the run() method with a condition: if wolverineAdrenaline is equals to 13.

Even though Yamaha YZF is the third thread in our order of execution, and has MIN_PRIORITY, there's no guarantee that it will be executed last for all JVM implementations.

You might also note that in this example we set the Dodge Tomahawk thread as daemon. Because it's a daemon thread, Dodge Tomahawk may never complete execution. But the other two threads are non-daemon by default, so the Harley Davidson and Yamaha YZF threads will definitely complete their execution.

To conclude, the result will be D: Indeterminate, because there is no guarantee that the thread scheduler will follow our order of execution or thread priority.

Remember, we can't rely on program logic (order of threads or thread priority) to predict the JVM's order of execution.

Video challenge! Debugging variable arguments

Debugging is one of the easiest ways to fully absorb programming concepts while also improving your code. In this video you can follow along while I debug and explain the thread behavior challenge:

Common mistakes with Java threads

  • Invoking the run() method to try to start a new thread.
  • Trying to start a thread twice (this will cause an IllegalThreadStateException).
  • Allowing multiple processes to change the state of an object when it shouldn't change.
  • Writing program logic that relies on thread priority (you can't predict it).
  • Relying on the order of thread execution--even if we start a thread first, there is no guarantee it will be executed first.

What to remember about Java threads

  • Invoke the start() method to start a Thread.
  • It's possible to extend the Thread class directly in order to use threads.
  • It's possible to implement a thread action inside a Runnable interface.
  • Thread priority depends on the JVM implementation.
  • Thread behavior will always depend on the JVM implementation.
  • A daemon thread won't complete if an enclosing non-daemon thread ends first.

Learn more about Java threads on JavaWorld

  • Read the Java 101 threads series to learn more about threads and runnables, thread synchronization, thread scheduling with wait/notify, and thread death.
  • Modern threading: A Java concurrency primer introduces java.util.concurrent and answers common questions for developers new to Java concurrency.
  • Modern threading for not-quite-beginners offers more advanced tips and best practices for working with java.util.concurrent.

More from Rafael

  • Get more quick code tips: Read all the posts in the Java Challengers series.
  • Build your Java skills: Visit the Java Dev Gym for a code workout.
  • Want to work on stress free projects and write bug-free code? Visit the NoBugsProject for your copy of No Bugs, No Stress - Create a Life-Changing Software Without Destroying Your Life.

Kisah ini, "Perilaku benang dalam JVM" pada awalnya diterbitkan oleh JavaWorld.