Java 101: Разбиране на Java нишките, Част 1: Представяне на нишки и изпълними

Тази статия е първата от четири части от серия Java 101 , изследващи Java нишки. Въпреки че си мислите, че нишките в Java ще бъдат предизвикателни за разбиране, възнамерявам да ви покажа, че нишките са лесни за разбиране. В тази статия ви запознавам с нишките и изпълнимите програми на Java. В следващите статии ще изследваме синхронизацията (чрез заключване), проблемите със синхронизацията (като блокиране), механизма за изчакване / известяване, планиране (с и без приоритет), прекъсване на нишки, таймери, променливост, групи нишки и локални променливи на нишките .

Имайте предвид, че тази статия (част от архивите на JavaWorld) беше актуализирана с нови списъци с кодове и изходен код за изтегляне през май 2013 г.

Разбиране на нишките на Java - прочетете цялата поредица

  • Част 1: Представяне на нишки и изпълними
  • Част 2: Синхронизация
  • Част 3: Планиране на нишки и изчакайте / уведомете
  • Част 4: Групи нишки и нестабилност

Какво е нишка?

Концептуално понятието за нишка не е трудно да се разбере: това е независим път на изпълнение чрез програмен код. Когато се изпълняват множество нишки, пътят на една нишка през един и същ код обикновено се различава от другите. Например, да предположим, че една нишка изпълнява еквивалента на байтов код на ifчаст от оператора if-else , докато друга нишка изпълнява еквивалента на байтов код на elseчастта. Как JVM следи изпълнението на всяка нишка? JVM дава на всяка нишка собствен стек за извикване на методи. В допълнение към проследяването на текущата инструкция за байтов код, стекът за извикване на метод проследява локални променливи, параметри, които JVM предава на метод, и връщаната стойност на метода.

Когато множество нишки изпълняват последователности от инструкции за байтов код в една и съща програма, това действие е известно като многопоточност . Многопоточността облагодетелства програмата по различни начини:

  • Многонишковите GUI (графичен потребителски интерфейс) програми остават отзивчиви за потребителите, докато изпълняват други задачи, като например повторно представяне или отпечатване на документ.
  • Програмите с резба обикновено завършват по-бързо от своите неконци. Това важи особено за нишки, работещи на многопроцесорна машина, където всяка нишка има свой собствен процесор.

Java постига многопоточност чрез своя java.lang.Threadклас. Всеки Threadобект описва една нишка на изпълнение. Това се случва в изпълнение Threadе run()метод. Тъй като run()методът по подразбиране не прави нищо, трябва да подкласирате Threadи замените, за run()да извършите полезна работа. За вкус от нишки и многопоточност в контекста Thread, разгледайте Листинг 1:

Листинг 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'); } } }

Листинг 1 представя изходния код на приложение, състоящо се от класове ThreadDemoи MyThread. Класът ThreadDemoзадвижва приложението, като създава MyThreadобект, стартира нишка, която се асоциира с този обект, и изпълнява някакъв код за отпечатване на таблица с квадрати. От друга страна, MyThreadима приоритет Threadе run()метод за отпечатване (на стандартния изходен поток) триъгълник под прав ъгъл, съставен от звездичка знаци.

Планиране на нишки и JVM

Повечето (ако не всички) реализации на JVM използват възможностите за нишки на основната платформа. Тъй като тези възможности са специфични за платформата, редът на изхода на вашите многонишкови програми може да се различава от реда на изхода на някой друг. Тази разлика е резултат от планирането, тема, която изследвам по-късно в тази поредица.

Когато пишете, за java ThreadDemoда стартирате приложението, JVM създава начална нишка на изпълнение, която изпълнява main()метода. Чрез изпълнение mt.start ();стартовата нишка казва на JVM да създаде втора нишка на изпълнение, която изпълнява инструкциите за байт код, съдържащи метода на MyThreadобекта run(). Когато start()методът се върне, стартовата нишка изпълнява своя forцикъл, за да отпечата таблица с квадрати, докато новата нишка изпълнява run()метода за отпечатване на правоъгълния триъгълник.

Как изглежда изходът? Бягайте, ThreadDemoза да разберете. Ще забележите, че изходът на всяка нишка има тенденция да се разпръсква с изхода на другия. Това води до това, защото и двете нишки изпращат изхода си към един и същ стандартен изходен поток.

Класът нишка

За да станете по-опитни в писането на многонишкови кодове, първо трябва да разберете различните методи, съставляващи Threadкласа. Този раздел изследва много от тези методи. По-конкретно, научавате за методи за стартиране на нишки, именуване на нишки, поставяне на нишки в режим на заспиване, определяне дали дадена нишка е жива, присъединяване на една нишка към друга нишка и изброяване на всички активни нишки в текущата група нишки и подгрупи. Също така обсъждам Threadпомощните програми за отстраняване на грешки и потребителските нишки спрямо демоновите нишки.

Ще представя останалата част от Threadметодите в следващите статии, с изключение на остарелите методи на Sun.

Остарели методи

Sun е отхвърлил различни Threadметоди, като suspend()и resume(), тъй като те могат да заключат вашите програми или да повредят обекти. В резултат на това не трябва да ги извиквате във вашия код. За справяне с тези методи се консултирайте с документацията на SDK. Не обхващам остарели методи в тази поредица.

Конструиране на нишки

Threadима осем конструктора. Най-простите са:

  • Thread(), който създава Threadобект с име по подразбиране
  • Thread(String name), който създава Threadобект с име, което nameаргументът указва

Следващите най-прости конструктори са Thread(Runnable target)и Thread(Runnable target, String name). Освен Runnableпараметрите, тези конструктори са идентични с гореспоменатите конструктори. Разликата: RunnableПараметрите идентифицират обекти отвън, Threadкоито предоставят run()методите. (Можете да научите за Runnableпо-късно в тази статия.) Окончателните четири конструкторите приличат Thread(String name), Thread(Runnable target)и Thread(Runnable target, String name); крайните конструктори обаче включват и ThreadGroupаргумент за организационни цели.

Един от последните четири конструктора, Thread(ThreadGroup group, Runnable target, String name, long stackSize)е интересен с това, че ви позволява да посочите желания размер на стека на метода за извикване на нишката. Възможността да се определи този размер се оказва полезна в програми с методи, които използват рекурсия - техника на изпълнение, при която методът многократно се самоизвиква - за елегантно решаване на определени проблеми. Като изрично зададете размера на стека, понякога можете да предотвратите StackOverflowErrors. Прекалено големият размер обаче може да доведе до OutOfMemoryErrors. Също така, Sun разглежда размера на стека на метода за извикване като зависим от платформата. В зависимост от платформата размерът на стека на метода може да се промени. Ето защо, помислете внимателно за последиците от вашата програма, преди да напишете код, който извиква Thread(ThreadGroup group, Runnable target, String name, long stackSize).

Стартирайте вашите превозни средства

Конците наподобяват превозни средства: те преместват програми от началото до края. Threadи Threadобектите на подкласа не са нишки. Вместо това те описват атрибутите на нишка, като името й, и съдържат код (чрез run()метод), който нишката изпълнява. Когато дойде време за изпълнение на нова нишка run(), друга нишка извиква метода на Threadобекта или неговия подклас start(). Например, за да стартирате втора нишка, стартовата нишка на приложението - която изпълнява - main()извиква start(). В отговор кодът за обработка на нишки на JVM работи с платформата, за да гарантира, че нишката правилно се инициализира и извиква метода на Threadобект или негов подклас run().

След като start()завърши, се изпълняват множество нишки. Тъй като сме склонни да мислим по линеен начин, често ни е трудно да разберем едновременната (едновременна) дейност, която възниква, когато се изпълняват две или повече нишки. Следователно трябва да разгледате диаграма, която показва къде се изпълнява нишка (нейната позиция) спрямо времето. Фигурата по-долу представя такава диаграма.

Диаграмата показва няколко значими периода от време:

  • Инициализация на началната нишка
  • В момента, в който нишката започне да се изпълнява main()
  • В момента, в който нишката започне да се изпълнява start()
  • Моментът start()създава нова нишка и се връща къмmain()
  • Инициализацията на новата нишка
  • В момента, в който новата нишка започне да се изпълнява run()
  • Различните моменти, в които всяка нишка завършва

Note that the new thread's initialization, its execution of run(), and its termination happen simultaneously with the starting thread's execution. Also note that after a thread calls start(), subsequent calls to that method before the run() method exits cause start() to throw a java.lang.IllegalThreadStateException object.

What's in a name?

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.

За демонстрация sleep(long millis)съм написал CalcPI1заявление. Това приложение стартира нова нишка, която използва математически алгоритъм за изчисляване на стойността на математическата константа pi. Докато новата нишка изчислява, началната нишка прави пауза за 10 милисекунди чрез извикване sleep(long millis). След като стартовата нишка се събуди, тя отпечатва стойността pi, която новата нишка съхранява в променлива pi. Листинг 3 представя CalcPI1изходния код:

Листинг 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"); } }

Ако стартирате тази програма, ще видите изход, подобен (но вероятно не идентичен) на следния:

pi = -0.2146197014017295 Finished calculating PI