Поведение на резбата в JVM

Threading се отнася до практиката на едновременно изпълнение на програмни процеси за подобряване на производителността на приложението. Въпреки че не е толкова често да се работи с нишки директно в бизнес приложения, те се използват през цялото време в Java рамки.

Като пример, рамките, които обработват голям обем информация, като Spring Batch, използват нишки за управление на данни. Манипулирането на нишки или процесори едновременно подобрява производителността, което води до по-бързи и по-ефективни програми.

Вземете изходния код

Вземете кода за този Java Challenger. Можете да провеждате свои собствени тестове, докато следвате примерите.

Намерете първата си нишка: методът main () на Java

Дори ако никога не сте работили директно с Java нишки, вие сте работили индиректно с тях, защото методът main () на Java съдържа основна нишка. Всеки път, когато сте изпълнили main()метода, сте изпълнили и основния Thread.

Изучаването на Threadкласа е много полезно за разбиране на начина, по който нишките работят в Java програми. Можем да получим достъп до нишката, която се изпълнява, като извикаме currentThread().getName()метода, както е показано тук:

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

Този код ще отпечата „main“, идентифицирайки нишката, която се изпълнява в момента. Знанието как да се идентифицира изпълняваната нишка е първата стъпка към усвояването на концепциите за нишките.

Жизненият цикъл на нишката Java

Когато работите с нишки, е изключително важно да знаете състоянието на нишката. Жизненият цикъл на нишката Java се състои от шест състояния на нишки:

  • Ново : Инстанцирано е ново Thread().
  • Изпълним : Методът на Thread' start()е извикан.
  • Изпълнява се : start()Методът е извикан и нишката се изпълнява.
  • Suspended : Нишката е временно спряна и може да бъде възобновена от друга нишка.
  • Блокиран : Конецът чака възможност за стартиране. Това се случва, когато една нишка вече е извикала synchronized()метода и следващата нишка трябва да изчака, докато приключи.
  • Прекратено : Изпълнението на нишката е завършено.
Рафаел Чинелато Дел Неро

Има още какво да изследвате и разберете за състоянията на нишките, но информацията на фигура 1 е достатъчна, за да разрешите това Java предизвикателство.

Едновременна обработка: Разширяване на клас Thread

В най-простата си, едновременна обработка се извършва чрез разширяване на Threadклас, както е показано по-долу.

 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"); } } 

Тук изпълняваме две нишки: MainThreadи InheritingThread. Когато извикаме start()метода с новия inheritingThread(), логиката в run()метода се изпълнява.

Също така предаваме името на втората нишка в Threadконструктора на класа, така че изходът ще бъде:

 main is running. inheritingThread is running. 

Интерфейсът Runnable

Вместо да използвате наследяване, можете да внедрите интерфейса Runnable. Преминаването Runnableвътре в Threadконструктор води до по-малко свързване и по-голяма гъвкавост. След преминаване Runnableможем да извикаме start()метода точно както направихме в предишния пример:

 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()); } } 

Недемон срещу демони нишки

По отношение на изпълнението има два вида нишки:

  • Недемонови нишки се изпълняват до края. Основната нишка е добър пример за не-демонова нишка. Кодът в main()ще бъде винаги изпълняван до края, освен ако не System.exit()принуди програмата да завърши.
  • А демон нишка е обратното, общо взето един процес, който не се изисква да бъдат изпълнени до края.

Запомнете правилото : Ако заграждаща не-демонова нишка завършва преди демонова нишка, демоновата нишка няма да бъде изпълнена до края.

За да разберете по-добре връзката между демони и не-демони нишки, изучете този пример:

 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()); } } 

В този пример използвах демонова нишка, за да декларирам диапазон от 1 до 100 000, да повторя всички тях и след това да отпечатам. Но помнете, демонова нишка няма да завърши изпълнението, ако основната нишка на недемона завърши първа.

Изходът ще продължи както следва:

  1. Начало на изпълнението в основната нишка.
  2. Отпечатайте числа от 1 до евентуално 100 000.
  3. Край на изпълнението в основната нишка, много вероятно преди завършване на итерация до 100 000.

Крайният изход ще зависи от вашата реализация на JVM.

И това ме отвежда до следващата ми точка: нишките са непредсказуеми.

Приоритет на нишката и JVM

Възможно е да се даде приоритет на изпълнението на нишка с setPriorityметода, но как се обработва зависи от изпълнението на JVM. Linux, MacOS и Windows имат различни внедрения на JVM и всеки ще обработва приоритета на нишката според собствените си настройки по подразбиране.

Приоритетът на нишката, който сте задали, обаче влияе върху реда на извикване на нишка. Трите константи, декларирани в Threadкласа, са:

 /** * 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.

Тази история „Поведение на нишките в JVM“ първоначално е публикувана от JavaWorld.