Perilaku utas di JVM

Threading mengacu pada praktik menjalankan proses pemrograman secara bersamaan untuk meningkatkan kinerja aplikasi. Meskipun tidak umum untuk bekerja dengan utas secara langsung dalam aplikasi bisnis, mereka digunakan sepanjang waktu dalam kerangka kerja Java.

Sebagai contoh, framework yang memproses informasi dalam jumlah besar, seperti Spring Batch, menggunakan utas untuk mengelola data. Memanipulasi utas atau proses CPU secara bersamaan meningkatkan kinerja, menghasilkan program yang lebih cepat dan lebih efisien.

Dapatkan kode sumbernya

Dapatkan kode untuk Java Challenger ini. Anda dapat menjalankan pengujian Anda sendiri sambil mengikuti contoh.

Temukan utas pertama Anda: metode main () Java

Meskipun Anda belum pernah bekerja secara langsung dengan thread Java, Anda telah bekerja dengannya secara tidak langsung karena metode main () Java berisi Thread utama. Kapan pun Anda telah menjalankan main()metode ini, Anda juga telah menjalankan utama Thread.

Mempelajari Threadkelas sangat membantu untuk memahami cara kerja threading di program Java. Kita dapat mengakses utas yang sedang dieksekusi dengan memanggil currentThread().getName()metode, seperti yang ditunjukkan di sini:

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

Kode ini akan mencetak "utama", mengidentifikasi utas yang saat ini sedang dijalankan. Mengetahui cara mengidentifikasi utas yang sedang dieksekusi adalah langkah pertama untuk menyerap konsep utas.

Siklus hidup thread Java

Saat bekerja dengan utas, sangat penting untuk menyadari status utas. Siklus hidup thread Java terdiri dari enam status thread:

  • Baru : Yang baru Thread()telah dibuat.
  • Runnable : The Thread's start()metode telah dipanggil.
  • Berjalan : start()Metode telah dipanggil dan utas sedang berjalan.
  • Ditangguhkan : Utas ditangguhkan sementara, dan dapat dilanjutkan dengan utas lain.
  • Diblokir : Untaian sedang menunggu peluang untuk dijalankan. Ini terjadi ketika satu utas telah memanggil synchronized()metode dan utas berikutnya harus menunggu hingga selesai.
  • Dihentikan : Eksekusi utas selesai.
Rafael Chinelato Del Nero

Ada lebih banyak hal untuk dijelajahi dan dipahami tentang status utas, tetapi informasi pada Gambar 1 sudah cukup bagi Anda untuk menyelesaikan tantangan Java ini.

Pemrosesan bersamaan: Memperluas kelas Thread

Paling sederhana, pemrosesan bersamaan dilakukan dengan memperluas Threadkelas, seperti yang ditunjukkan di bawah ini.

 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 kami menjalankan dua utas: the MainThreaddan the InheritingThread. Saat kita memanggil start()metode dengan yang baru inheritingThread(), logika dalam run()metode tersebut dieksekusi.

Kami juga meneruskan nama utas kedua di Threadkonstruktor kelas, sehingga hasilnya adalah:

 main is running. inheritingThread is running. 

Antarmuka Runnable

Daripada menggunakan warisan, Anda dapat mengimplementasikan antarmuka Runnable. Melewati Runnablebagian dalam Threadkonstruktor menghasilkan lebih sedikit kopling dan lebih banyak fleksibilitas. Setelah lulus Runnable, kita bisa memanggil start()metode ini persis seperti yang kita lakukan di 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()); } } 

Untaian non-daemon vs daemon

Dalam hal eksekusi, ada dua jenis utas:

  • Untaian non-daemon dijalankan hingga akhir. Utas utama adalah contoh bagus utas non-daemon. Kode dalam main()akan selalu dieksekusi hingga akhir, kecuali program System.exit()memaksa untuk diselesaikan.
  • Sebuah benang daemon adalah berlawanan, pada dasarnya sebuah proses yang tidak diperlukan akan dieksekusi sampai akhir.

Ingat aturannya : Jika utas non-daemon penutup berakhir sebelum utas daemon, utas daemon tidak akan dijalankan sampai akhir.

Untuk lebih memahami hubungan untaian daemon dan non-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 utas daemon untuk menyatakan rentang dari 1 hingga 100.000, mengulang semuanya, lalu mencetak. Tapi ingat, utas daemon tidak akan menyelesaikan eksekusi jika utas utama non-daemon selesai lebih dulu.

Outputnya akan dilanjutkan sebagai berikut:

  1. Mulai eksekusi di utas utama.
  2. Cetak nomor dari 1 hingga mungkin 100.000.
  3. Akhir eksekusi di thread utama, kemungkinan besar sebelum iterasi mencapai 100.000 selesai.

Hasil akhir akan bergantung pada implementasi JVM Anda.

Dan itu membawa saya ke poin saya berikutnya: utas tidak dapat diprediksi.

Prioritas utas dan JVM

Eksekusi thread mungkin saja diprioritaskan dengan setPrioritymetode ini, tetapi cara penanganannya bergantung pada implementasi JVM. Linux, MacOS, dan Windows semuanya memiliki implementasi JVM yang berbeda, dan masing-masing akan menangani prioritas utas sesuai dengan defaultnya sendiri.

Namun, prioritas utas yang Anda tetapkan memengaruhi urutan pemanggilan utas. Tiga konstanta yang dideklarasikan di 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.

Artikel ini, "Perilaku Thread di JVM", awalnya diterbitkan oleh JavaWorld.