Java 101: Разбиране на нишките Java, Част 3: Планиране на нишки и изчакване / известяване

Този месец продължавам своето въведение от четири части в нишките Java, като се фокусирам върху планирането на нишки, механизма за изчакване / известяване и прекъсването на нишките. Ще проучите как JVM или планировчикът на нишки на операционната система избира следващата нишка за изпълнение. Както ще откриете, приоритетът е важен за избора на планировчик на нишки. Ще разгледате как една нишка чака, докато не получи известие от друга нишка, преди да продължи изпълнението и ще научите как да използвате механизма за изчакване / известяване за координиране на изпълнението на две нишки в отношенията производител-потребител. И накрая, ще научите как преждевременно да събудите спяща или чакаща нишка за прекратяване на нишка или други задачи. Също така ще ви науча как нишка, която не е нито спяща, нито чакаща, открива заявка за прекъсване от друга нишка.

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

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

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

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

В идеализиран свят всички програмни нишки ще имат свои собствени процесори, на които да работят. Докато не дойде времето, когато компютрите имат хиляди или милиони процесори, нишките често трябва да споделят един или повече процесори. Или JVM, или операционната система на основната платформа дешифрират как да споделят ресурса на процесора между нишките - задача, известна като планиране на нишки . Тази част от JVM или операционната система, която извършва планиране на нишки, е планировчик на нишки .

Забележка: За да опростя дискусията си за планиране на нишки, аз се фокусирам върху планирането на нишки в контекста на един процесор. Можете да екстраполирате тази дискусия на множество процесори; Оставям тази задача на вас.

Не забравяйте две важни точки относно планирането на нишки:

  1. Java не принуждава виртуална машина да планира нишки по определен начин или да съдържа планировчик на нишки. Това предполага планиране на нишки в зависимост от платформата. Следователно трябва да проявявате внимание при писането на Java програма, чието поведение зависи от начина на планиране на нишките и трябва да работи последователно на различни платформи.
  2. За щастие, когато пишете Java програми, трябва да помислите как Java планира нишки само когато поне една от нишките на вашата програма използва процесора за дълги периоди от време и междинните резултати от изпълнението на тази нишка се окажат важни. Например, аплетът съдържа нишка, която динамично създава изображение. Периодично искате нишката за рисуване да изчертава текущото съдържание на това изображение, така че потребителят да може да види как изображението напредва. За да сте сигурни, че нишката за изчисление не монополизира процесора, помислете за планиране на нишка.

Проучете програма, която създава две нишки с интензивен процесор:

Листинг 1. SchedDemo.java

// SchedDemo.java class SchedDemo { public static void main (String [] args) { new CalcThread ("CalcThread A").start (); new CalcThread ("CalcThread B").start (); } } class CalcThread extends Thread { CalcThread (String name) { // Pass name to Thread layer. super (name); } double calcPI () { boolean negative = true; double pi = 0.0; 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; return pi; } public void run () { for (int i = 0; i < 5; i++) System.out.println (getName () + ": " + calcPI ()); } }

SchedDemoсъздава две нишки, всяка от които изчислява стойността на pi (пет пъти) и отпечатва всеки резултат. В зависимост от това как вашето внедряване на JVM планира нишки, може да видите изход, наподобяващ следното:

CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

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

CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

Горният изход показва планировщика на нишки, благоприятстващ една нишка пред друга. Двата изхода по-горе илюстрират две общи категории планиращи нишки: зелен и естествен. Ще проуча техните поведенчески разлики в следващите раздели. Докато обсъждаме всяка категория, се позовавам на състояния на нишките, от които има четири:

  1. Начално състояние: Програмата е създала обект на нишка на нишка, но нишката все още не съществува, тъй като start()методът на обекта на нишката все още не е извикан.
  2. Изпълнимо състояние: Това е състояние по подразбиране на нишката. След приключването на извикването start()потокът става изпълним, независимо дали се изпълнява или не, т.е. използва процесора. Въпреки че много нишки могат да се изпълняват, в момента се изпълнява само една. Планиращите нишки определят коя изпълнима нишка да се присвои на процесора.
  3. Блокирано състояние: Когато нишка изпълнява sleep(), wait()или join()методи, когато даден опити конец, за да четат данни още не са налични от мрежа, а когато нишката чака за придобиване на заключване, че нишката е в блокирано състояние: това не е нито работи, нито в позиция за бягане. (Вероятно можете да мислите за други моменти, когато дадена нишка ще изчака нещо да се случи.) Когато блокираната нишка се отблокира, тази нишка се премества в състояние за изпълнение.
  4. Крайно състояние: След като изпълнението остави run()метода на нишката , тази нишка е в крайно състояние. С други думи, нишката престава да съществува.

Как планировчикът на нишките избира коя изпълнима нишка да изпълни? Започвам да отговарям на този въпрос, докато обсъждам планирането на зелени нишки. Завършвам отговора, докато обсъждам планиране на родна нишка.

График на зелена нишка

Не всички операционни системи, например, древната система на Microsoft Windows 3.1, поддържащи нишки, поддържат нишки. За такива системи Sun Microsystems може да проектира JVM, който разделя единствената си нишка на изпълнение на множество нишки. JVM (не операционната система на основната платформа) предоставя логиката на резбата и съдържа планиращия поток. JVM нишките са зелени нишки или потребителски нишки .

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

Забележка: Текуща нишка с най-висок приоритет не винаги ще се изпълнява. Ето приоритета на езиковата спецификация на Java :

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

Това признание говори много за внедряването на JVM със зелена нишка. Тези JVM не могат да си позволят да позволят на нишките да блокират, защото това би обвързало единствената нишка на изпълнение на JVM. Следователно, когато нишката трябва да блокира, например когато тази нишка чете данни бавно, за да пристигне от файл, JVM може да спре изпълнението на нишката и да използва механизъм за анкетиране, за да определи кога данните пристигат. Докато нишката остава спряна, планировчикът на нишки на JVM може да насрочи изпълнение на нишка с по-нисък приоритет. Да предположим, че данните пристигат, докато нишката с по-нисък приоритет работи. Въпреки че нишката с по-висок приоритет трябва да се стартира веднага щом пристигнат данните, това не се случва, докато JVM следващата анкета операционната система и открие пристигането. Следователно нишката с по-нисък приоритет се изпълнява, въпреки че нишката с по-висок приоритет трябва да работи.трябва да се тревожите за тази ситуация само когато имате нужда от поведение в реално време от Java. Но тогава Java не е операционна система в реално време, така че защо да се притеснявате?

За да разберете коя управляема зелена нишка става текущата зелена нишка, помислете за следното. Да предположим, че вашето приложение се състои от три нишки: основната нишка, която изпълнява main()метода, нишка за изчисление и нишка, която чете въвеждане от клавиатурата. Когато няма въвеждане от клавиатурата, нишката за четене се блокира. Да приемем, че нишката за четене има най-висок приоритет, а нишката за изчисление има най-нисък приоритет. (За по-простота, също така приемете, че няма налични други вътрешни JVM нишки.) Фигура 1 илюстрира изпълнението на тези три нишки.

По време на T0 основната нишка започва да работи. По време на T1 основната нишка стартира нишката за изчисление. Тъй като нишката за изчисление има по-нисък приоритет от основната нишка, нишката за изчисление чака процесора. По време на T2 основната нишка стартира нишката за четене. Тъй като нишката за четене има по-висок приоритет от основната нишка, основната нишка чака процесора, докато нишката за четене работи. По време на Т3 нишката за четене блокира и основната нишка работи. По време на Т4 нишката за четене се отблокира и стартира; основната нишка чака. И накрая, в момент T5, нишката за четене блокира и основната нишка работи. Това редуване в изпълнение между четенето и основните нишки продължава, докато програмата работи. Нишката за изчисление никога не се изпълнява, защото има най-нисък приоритет и по този начин гладува за вниманието на процесора,ситуация, известна катоглад на процесора .

Можем да променим този сценарий, като дадем на нишката за изчисление същия приоритет като основната нишка. Фигура 2 показва резултата, започвайки с времето T2. (Преди T2, фигура 2 е идентична с фигура 1.)

По време на Т2 нишката за четене се изпълнява, докато основната и изчислителната нишки чакат процесора. По време на Т3 нишката за четене блокира и нишката за изчисление се изпълнява, тъй като основната нишка се изпълнява точно преди нишката за четене. По време на Т4 нишката за четене се отблокира и стартира; основните и изчислителните нишки изчакват. По време на T5 нишката за четене блокира и основната нишка се изпълнява, тъй като нишката за изчисляване се изпълнява точно преди нишката за четене. Това редуване в изпълнението между основната и изчислителната нишки продължава, докато програмата работи и зависи от по-високоприоритетната нишка, която се изпълнява и блокира.

We must consider one last item in green thread scheduling. What happens when a lower-priority thread holds a lock that a higher-priority thread requires? The higher-priority thread blocks because it cannot get the lock, which implies that the higher-priority thread effectively has the same priority as the lower-priority thread. For example, a priority 6 thread attempts to acquire a lock that a priority 3 thread holds. Because the priority 6 thread must wait until it can acquire the lock, the priority 6 thread ends up with a 3 priority—a phenomenon known as priority inversion.

Приоритетната инверсия може значително да забави изпълнението на нишка с по-висок приоритет. Да предположим например, че имате три нишки с приоритети 3, 4 и 9. Нишката с приоритет 3 работи, а останалите нишки са блокирани. Да приемем, че нишката с приоритет 3 грабва ключалка, а нишката с приоритет 4 се отблокира. Нишката с приоритет 4 става текущата нишка. Тъй като нишката с приоритет 9 изисква заключване, тя продължава да чака, докато нишката с приоритет 3 освободи заключването. Нишката с приоритет 3 обаче не може да освободи ключалката, докато нишката с приоритет 4 не блокира или прекрати. В резултат на това нишката с приоритет 9 забавя изпълнението си.