CodeGym /Java блог /Случаен /Управление на нишки. Променливата ключова дума и методът ...
John Squirrels
Ниво
San Francisco

Управление на нишки. Променливата ключова дума и методът yield().

Публикувано в групата
здрасти Продължаваме нашето изследване на многопоточността. Днес ще се запознаем с volatileключовата дума и yield()метода. Нека се потопим :)

Нестабилната ключова дума

Когато създаваме многонишкови applications, можем да се сблъскаме с два сериозни проблема. Първо, когато се изпълнява многонишково приложение, различни нишки могат да кешират стойностите на променливите (вече говорихме за това в урока, озаглавен „Използване на volatile“ ). Може да имате ситуация, в която една нишка променя стойността на променлива, но втора нишка не вижда промяната, защото работи с нейното кеширано копие на променливата. Естествено, последствията могат да бъдат сериозни. Да предположим, че това не е просто няHowва стара променлива, а по-скоро салдото по вашата банкова сметка, което изведнъж започва произволно да скача нагоре-надолу :) Това не звучи забавно, нали? Второ, в Java операциите за четене и писане на всички примитивни типове,longdouble, са атомни. Ами, например, ако промените стойността на intпроменлива в една нишка и в друга нишка прочетете стойността на променливата, ще получите or старата й стойност, or новата, т.е. стойността, която е резултат от промяната в нишка 1. Няма "междинни стойности". Това обаче не работи с longs и doubles. Защо? Поради поддръжката на различни платформи. Помните ли в началните нива, че казахме, че водещият принцип на Java е „пиши веднъж, изпълнявай навсякъде“? Това означава поддръжка на различни платформи. С други думи, Java приложение работи на всяHowви различни платформи. Например на операционни системи Windows, различни версии на Linux or MacOS. Ще работи безпроблемно на всички тях. С тегло 64 бита,longdoubleса „най-тежките“ примитиви в Java. А някои 32-битови платформи просто не прилагат атомарно четене и запис на 64-битови променливи. Такива променливи се четат и записват с две операции. Първо, първите 32 бита се записват в променливата, а след това се записват още 32 бита. В резултат на това може да възникне проблем. Една нишка записва няHowва 64-битова стойност в Xпроменлива и го прави в две операции. В същото време втора нишка се опитва да прочете стойността на променливата и го прави между тези две операции - когато първите 32 бита са бor записани, но вторите 32 бита не са. В резултат на това той чете междинна, неправилна стойност и имаме грешка. Например, ако на такава платформа се опитаме да напишем номера на 9223372036854775809 към променлива, тя ще заема 64 бита. В двоична форма изглежда така: 100000000000000000000000000000000000000000000000000000001 Първата нишка започва да записва числото в променливата. Първоначално записва първите 32 бита ( 100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 ) И втората нишка може да се вклини между тези операции, четейки междинната стойност на променливата (100000000000000000000000000000000000000000000000), които са първите 32 бита, които вече са записани. В десетичната система това число е 2 147 483 648. С други думи, просто искахме да запишем числото 9223372036854775809 в променлива, но поради факта, че тази операция не е атомарна на някои платформи, имаме злото число 2,147,483,648, което се появи от нищото и ще има неизвестен ефект върху програма. Втората нишка просто прочете стойността на променливата, преди да е приключила записването й, т.е. нишката видя първите 32 бита, но не и вторите 32 бита. Разбира се, тези проблеми не са възникнали вчера. Java ги решава с една ключова дума: volatile. Ако използвамеvolatileключова дума, когато декларираме няHowва променлива в нашата програма...

public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…означава, че:
  1. Винаги ще се чете и пише атомарно. Дори ако е 64-битов doubleor long.
  2. Java машината няма да го кешира. Така че няма да имате ситуация, в която 10 нишки работят със собствените си локални копия.
Така само с една дума се решават два много сериозни проблема :)

Методът yield().

Вече прегледахме много от Threadметодите на класа, но има един важен, който ще бъде нов за вас. Това е yield()методът . И прави точно това, което подсказва името му! Управление на нишки.  Ключовата дума volatile и методът yield() - 2Когато извикаме yieldметода на нишка, той всъщност говори с другите нишки: „Хей, момчета. Не бързам особено за никъде, така че ако за някой от вас е важно да получи процесорно време, вземете го — мога да изчакам. Ето прост пример How работи това:

public class ThreadExample extends Thread {

   public ThreadExample() {
       this.start();
   }

   public void run() {

       System.out.println(Thread.currentThread().getName() + " yields its place to others");
       Thread.yield();
       System.out.println(Thread.currentThread().getName() + " has finished executing.");
   }

   public static void main(String[] args) {
       new ThreadExample();
       new ThreadExample();
       new ThreadExample();
   }
}
Ние последователно създаваме и стартираме три нишки: Thread-0, Thread-1и Thread-2. Thread-0започва пръв и веднага отстъпва на останалите. След това Thread-1се стартира и също дава. След това Thread-2се стартира, което също дава. Нямаме повече нишки и след като Thread-2даде последното си място, планировчикът на нишки казва: „Хм, няма повече нови нишки. Кого имаме на опашката? Кой отстъпи мястото си преди Thread-2? Изглежда, че беше Thread-1. Добре, това означава, че ще го оставим да работи. Thread-1завършва работата си и след това планировчикът на нишки продължава своята координация: „Добре, Thread-1готово. Имаме ли още някой на опашката?“. Нишка-0 е в опашката: отстъпи мястото си точно предиThread-1. Сега идва редът му и се изпълнява докрай. След това планировчикът завършва координирането на нишките: „Добре, Thread-2, отстъпихте на други нишки и всички те вече са готови. Ти беше последният, който се предаде, така че сега е твой ред. След това Thread-2работи до завършване. Конзолният изход ще изглежда така: Нишка-0 отстъпва мястото си на други Нишка-1 отстъпва мястото си на други Нишка-2 отстъпва мястото си на други Нишка-1 е завършила изпълнението. Thread-0 завърши изпълнението. Thread-2 завърши изпълнението. Разбира се, планировчикът на нишки може да стартира нишките в различен ред (например 2-1-0 instead of 0-1-2), но принципът остава същият.

Случва се - преди правила

Последното нещо, което ще засегнем днес, е понятието „ случва се преди “. Както вече знаете, в Java планировчикът на нишки изпълнява по-голямата част от работата, свързана с разпределянето на време и ресурси на нишките за изпълнение на техните задачи. Също така многократно сте виждали How нишките се изпълняват в произволен ред, който обикновено е невъзможно да се предвиди. И като цяло, след „последователното“ програмиране, което направихме преди, многопоточното програмиране изглежда като нещо произволно. Вече сте повярвали, че можете да използвате множество методи за контрол на потока на многонишкова програма. Но многонишковостта в Java има още един стълб — 4-те правила „ случва се преди “. Разбирането на тези правила е съвсем просто. Представете си, че имаме две нишки — AиB. Всяка от тези нишки може да изпълнява операции 1и 2. Във всяко правило, когато казваме „ А се случва преди Б “, имаме предвид, че всички промени, напequalsи от нишката Aпреди операцията, 1и промените, произтичащи от тази операция, са видими за нишката, Bкогато операцията 2е извършена и след това. Всяко правило гарантира, че когато пишете многонишкова програма, определени събития ще се появят преди други в 100% от времето и че по време на работа 2нишката Bвинаги ще бъде наясно с промените, които нишката Aе направила по време на работа 1. Нека ги прегледаме.

Правило 1.

Освобождаването на mutex се случва преди същият монитор да бъде придобит от друга нишка. Мисля, че разбирате всичко тук. Ако мутексът на обект or клас е придобит от една нишка, например от нишка A, друга нишка (нишка B) не може да го придобие по същото време. Трябва да изчака, докато мютексът бъде освободен.

Правило 2.

Методът Thread.start()се случва преди Thread.run() . Отново няма нищо трудно тук. Вече знаете, че за да започнете да изпълнявате codeа вътре в run()метода, трябва да извикате start()метода в нишката. По-конкретно методът за стартиране, а не run()самият метод! Това правило гарантира, че стойностите на всички променливи, зададени преди Thread.start()извикването, ще бъдат видими в run()метода, след като той започне.

Правило 3.

Краят на run()метода се случва преди връщането от join()метода. Да се ​​върнем към нашите две теми: Aи B. Извикваме join()метода, така че нишката Bгарантирано ще изчака завършването на нишката, Aпреди да свърши работата си. Това означава, че методът на обекта A run()гарантирано ще се изпълнява до самия край. И всички промени в данните, които се случват в run()метода на нишката, Aса сто процента гарантирани, че ще бъдат видими в нишката, Bслед като приключи, чакайки нишката Aда завърши работата си, за да може да започне собствената си работа.

Правило 4.

Записването в volatileпроменлива се случва преди четене от същата променлива. Когато използваме volatileключовата дума, всъщност винаги получаваме текущата стойност. Дори с longor double(говорихме по-рано за проблемите, които могат да възникнат тук). Както вече разбирате, промените, напequalsи в някои нишки, не винаги са видими за други нишки. Но, разбира се, има много чести ситуации, в които подобно поведение не ни устройва. Да предположим, че присвояваме стойност на променлива в нишка A:

int z;

….

z = 555;
Ако нашата Bнишка трябва да покаже стойността на zпроменливата на конзолата, тя лесно може да покаже 0, защото не знае за присвоената стойност. Но правило 4 гарантира, че ако декларираме променливата zкато volatile, тогава промените в нейната стойност в една нишка винаги ще бъдат видими в друга нишка. Ако добавим към думата volatileкъм предишния code...

volatile int z;

….

z = 555;
... тогава предотвратяваме ситуацията, при която нишката Bможе да показва 0. Записването в volatileпроменливи се случва преди четене от тях.
Коментари
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION