Кіріспе
Сонымен, біз Java-да ағындар бар екенін білеміз, олар туралы « Жаваны жіппен бүлдіре алмайсыз: I бөлім - тақырыптар » шолуынан оқуға болады . Жұмысты бір уақытта орындау үшін жіптер қажет. Сондықтан жіптердің бір-бірімен қандай да бір түрде әрекеттесуі өте ықтимал. Мұның қалай болатынын және бізде қандай негізгі бақылаулар бар екенін анықтайық.Өткізіп жібер
Thread.yield() әдісі жұмбақ және сирек қолданылады. Интернетте оның сипаттамасының көптеген нұсқалары бар. Кейбіреулер олардың басымдықтарын ескере отырып, ағын төмен жылжитын ағындардың кезегі туралы жазады. Біреу ағын өзінің күйін іске қосылған күйден іске қосылатын күйге өзгертетінін жазады (бірақ бұл күйлерге ешқандай бөлу жоқ, ал Java оларды ажыратпайды). Бірақ іс жүзінде бәрі әлдеқайда белгісіз және белгілі бір мағынада қарапайым. Әдіс құжаттамасы тақырыбында " JDK-6416721: (ерекше ағын) Fix Thread.yield() javadocyield
" қатесі бар . Егер сіз оны оқысаңыз, шын мәнінде әдіс Java ағынын жоспарлаушыға бұл ағынды орындауға аз уақыт беруге болатыны туралы кейбір ұсыныстарды ғана беретіні анық . Бірақ іс жүзінде не болады, жоспарлаушы ұсынысты ести ме және оның не істейтіні JVM мен операциялық жүйенің іске асырылуына байланысты. Немесе басқа факторларға байланысты болуы мүмкін. Барлық шатасулар Java тілін дамыту кезінде көп ағынды қайта қарауға байланысты болуы мүмкін. Толығырақ " Java Thread.yield() қысқаша кіріспе " шолудан оқи аласыз . yield
Ұйқы - ұйықтап жатқан жіп
Жіп оны орындау кезінде ұйықтап кетуі мүмкін. Бұл басқа ағындармен әрекеттесудің ең қарапайым түрі. Java виртуалды машинасы орнатылған, Java codeы орындалатын операциялық жүйеде Thread Scheduler деп аталатын өзінің ағынды жоспарлаушысы бар. Қай жіпті қашан іске қосу керектігін өзі шешеді. Бағдарламалаушы бұл жоспарлаушымен тікелей Java codeынан өзара әрекеттесе алмайды, бірақ ол JVM арқылы жоспарлаушыдан ағынды біраз уақытқа кідіртуді, оны «ұйқы режиміне қоюды» сұрай алады. Толығырақ " Thread.sleep() " және " Multithreading қалай жұмыс істейді " мақалаларынан оқи аласыз . Сонымен қатар, Windows ОЖ жүйесінде ағындардың қалай жұмыс істейтінін білуге болады: « Windows Thread ішкі элементтері ». Енді оны өз көзімізбен көретін боламыз. Келесі codeты файлға сақтайықHelloWorldApp.java
:
class HelloWorldApp {
public static void main(String []args) {
Runnable task = () -> {
try {
int secToWait = 1000 * 60;
Thread.currentThread().sleep(secToWait);
System.out.println("Waked up");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(task);
thread.start();
}
}
Көріп отырғаныңыздай, бізде 60 секунд күтетін тапсырма бар, содан кейін бағдарлама аяқталады. Біз құрастырамыз javac HelloWorldApp.java
және іске қосамыз java HelloWorldApp
. Бөлек терезеде іске қосқан дұрыс. Мысалы, Windows жүйесінде бұл келесідей болады: start java HelloWorldApp
. jps пәрменін пайдалана отырып, процестің PID codeын анықтаймыз және ағындар тізімін келесі арқылы ашамыз jvisualvm --openpid pidПроцесса
: Көріп отырғаныңыздай, біздің ағын ұйқы күйіне өтті. Шын мәнінде, ағымдағы жіпті ұйықтау әлдеқайда әдемі болуы мүмкін:
try {
TimeUnit.SECONDS.sleep(60);
System.out.println("Waked up");
} catch (InterruptedException e) {
e.printStackTrace();
}
Біз барлық жерде өңдейтінімізді байқаған шығарсыз InterruptedException
? Неге екенін түсінейік.
Жіпті үзу немесе Thread.interrupt
Мәселе мынада, жіп ұйқыда күтіп тұрғанда, біреу бұл күтуді тоқтатқысы келуі мүмкін. Бұл жағдайда біз мұндай ерекшелікті өңдейміз.Thread.stop
Бұл әдіс ескірген деп жарияланғаннан кейін жасалды , яғни. ескірген және пайдалану қажет емес. Мұның себебі, әдісті шақырған кезде stop
жіп жай ғана «өлтірілді», бұл өте күтпеген еді. Біз ағынның қашан тоқтайтынын біле алмадық, деректердің сәйкестігіне кепілдік бере алмадық. Сіз файлға деректерді жазып жатырсыз, содан кейін ағын жойылады деп елестетіңіз. Сондықтан олар ағынды өлтірмей, оны үзу керек деп хабарлау қисындырақ деп шешті. Бұған қалай әрекет ету ағынның өзіне байланысты. Қосымша мәліметтерді Oracle компаниясының " Thread.stop неге ескірген? " Мысал қарастырайық:
public static void main(String []args) {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(60);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
Бұл мысалда біз 60 секунд күтпейміз, бірақ бірден «Үзілген» деп басып шығарамыз. Себебі біз ағын әдісі деп атадық interrupt
. Бұл әдіс «үзу күйі деп аталатын ішкі жалауды» орнатады. Яғни, әрбір жіп тікелей қол жеткізуге болмайтын ішкі жалаушаға ие. Бірақ бізде бұл жалаумен әрекеттесу үшін жергілікті әдістер бар. Бірақ бұл жалғыз жол емес. Жіп орындалу процесінде болуы мүмкін, бірдеңені күтпей, жай ғана әрекеттерді орындайды. Бірақ бұл олардың жұмысының белгілі бір кезеңінде оны аяқтағысы келетінін қамтамасыз ете алады. Мысалы:
public static void main(String []args) {
Runnable task = () -> {
while(!Thread.currentThread().isInterrupted()) {
//Do some work
}
System.out.println("Finished");
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
Жоғарыдағы мысалда цикл while
сырттан үзілгенше орындалатынын көруге болады. isInterrupted жалаушасы туралы білу маңызды нәрсе , егер біз оны ұстасақ InterruptedException
, жалауша isInterrupted
қалпына келтіріледі, содан кейін isInterrupted
ол false мәнін қайтарады. Сондай-ақ Thread сыныбы үшін тек ағымдағы ағынға қолданылатын статикалық әдіс бар - Thread.interrupted() , бірақ бұл әдіс жалаушаны "false" мәніне қайтарады! Толығырақ « Жіпті үзу » тарауынан оқи аласыз .
Қосылу — Басқа ағынның аяқталуын күтуде
Күтудің ең қарапайым түрі - басқа ағынның аяқталуын күту.public static void main(String []args) throws InterruptedException {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.join();
System.out.println("Finished");
}
Бұл мысалда жаңа ағын 5 секунд ұйықтайды. Бұл кезде негізгі жіп ұйықтап жатқан жіп оянып, жұмысын аяқтағанша күтеді. JVisualVM арқылы қарасаңыз, ағынның күйі келесідей болады: Бақылау құралдарының арқасында ағынмен не болып жатқанын көруге болады. Әдіс join
өте қарапайым, себебі бұл жай ғана java codeы бар әдіс, ол wait
шақырылған ағын тірі кезінде орындалады. Жіп өлгеннен кейін (тоқтату кезінде) күту тоқтатылады. Бұл әдістің бүкіл сиқыры join
. Сондықтан, ең қызықты бөлігіне көшейік.
Монитор тұжырымдамасы
Көп ағында Монитор деген нәрсе бар. Жалпы, монитор сөзі латын тілінен аударғанда «қадағалаушы» немесе «бақылаушы» дегенді білдіреді. Осы мақаланың аясында біз мәнін есте сақтауға тырысамыз, ал қалайтындар үшін егжей-тегжейлі ақпарат алу үшін сілтемелерден материалға енуіңізді сұраймын. Саяхатымызды Java тілінің спецификациясынан, яғни JLS арқылы бастайық: " 17.1. Синхрондау ". Онда мыналар айтылады: Жіптер арасында синхрондау мақсатында Java «Монитор» деп аталатын белгілі бір механизмді пайдаланады. Әрбір нысанда онымен байланыстырылған монитор бар және ағындар оны құлыптауы немесе құлпын ашуы мүмкін. Әрі қарай, біз Oracle веб-сайтында оқу құралын табамыз: « Ішкі құлыптар және синхрондау ». Бұл оқулық Java жүйесінде синхрондау ішкі құлып немесе монитор құлпы деп аталатын ішкі нысанның айналасында құрылатынын түсіндіреді. Көбінесе мұндай құлып жай ғана «монитор» деп аталады. Сондай-ақ, біз Java-дағы әрбір нысанның онымен байланысты ішкі құлыпқа ие екенін тағы да көреміз. Сіз « Java - ішкі құлыптар мен синхрондау » оқуға болады . Әрі қарай, Java тіліндегі нысанды монитормен қалай байланыстыруға болатындығын түсіну маңызды. Java тіліндегі әрбір нысанның тақырыбы бар - codeтан бағдарламашыға қол жетімді емес, бірақ виртуалды машина нысандармен дұрыс жұмыс істеу үшін қажет ішкі метадеректер түрі. Нысан тақырыбына келесідей MarkWord кіреді:https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf
public class HelloWorld{
public static void main(String []args){
Object object = new Object();
synchronized(object) {
System.out.println("Hello World");
}
}
}
Сонымен, кілт сөзді пайдалана отырып, synchronized
ағымдағы ағын (осы code жолдары орындалатын) нысанмен байланысты мониторды пайдалануға тырысады object
және «құлып алу» немесе «мониторды басып алу» (екінші нұсқа тіпті жақсырақ). Мониторға қатысты дау болмаса (яғни сол нысанда басқа ешкім синхрондауды қаламаса), Java «біржақты құлыптау» деп аталатын оңтайландыруды орындауға тырысуы мүмкін. Mark Word бағдарламасындағы нысанның тақырыбы сәйкес тег пен монитор қай ағынға тіркелген жазбадан тұрады. Бұл мониторды түсіру кезіндегі үстеме шығынды азайтады. Егер монитор бұрын басқа жіпке байланған болса, онда бұл құлыптау жеткіліксіз. JVM келесі құлыптау түріне ауысады - негізгі құлыптау. Ол салыстыру және ауыстыру (CAS) операцияларын пайдаланады. Сонымен қатар, Mark Word бағдарламасындағы тақырып енді Mark Word-тың өзін сақтамайды, бірақ оның қоймасына сілтеме + тег JVM негізгі құлыптауды қолданып жатқанымызды түсінуі үшін өзгертілді. Егер бірнеше ағындардың мониторы үшін келіспеушілік туындаса (біреуі мониторды басып алды, екіншісі монитордың босатылуын күтуде), онда Mark Word бағдарламасындағы тег өзгереді және Mark Word мониторға сілтеме ретінде сақтай бастайды. an object - JVM кейбір ішкі нысаны. JEP құжатында айтылғандай, бұл жағдайда осы нысанды сақтау үшін Native Heap жады аймағында бос орын қажет. Осы ішкі нысанның сақтау орнына сілтеме Mark Word нысанында орналасады. Осылайша, көріп отырғанымыздай, монитор шын мәнінде ортақ ресурстарға бірнеше ағындардың қол жеткізуін синхрондауды қамтамасыз ету механизмі болып табылады. JVM ауыстыратын осы механизмнің бірнеше іске асырылуы бар. Сондықтан, қарапайымдылық үшін, монитор туралы айтқанда, біз шын мәнінде құлыптар туралы айтып отырмыз.
Синхрондалған және құлыптау арқылы күтуде
Монитор түсінігі, біз бұрын көргеніміздей, «синхрондау блогы» (немесе оны сыни бөлім деп те атайды) түсінігімен тығыз байланысты. Мысал қарастырайық:public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Runnable task = () -> {
synchronized (lock) {
System.out.println("thread");
}
};
Thread th1 = new Thread(task);
th1.start();
synchronized (lock) {
for (int i = 0; i < 8; i++) {
Thread.currentThread().sleep(1000);
System.out.print(" " + i);
}
System.out.println(" ...");
}
}
Мұнда негізгі ағын алдымен тапсырманы жаңа ағынға жібереді, содан кейін бірден құлыпты «ұстап алады» және онымен ұзақ әрекетті орындайды (8 секунд). Осы уақыт ішінде тапсырма оның орындалуы үшін блокқа кіре алмайды synchronized
, өйткені құлып әлдеқашан бос. Егер жіп құлыпты ала алмаса, ол оны мониторда күтеді. Оны алған бойда ол орындауды жалғастырады. Монитордан жіп шыққанда, ол құлыпты босатады. JVisualVM-де ол келесідей болады: Көріп отырғаныңыздай, JVisualVM-дегі күй «Монитор» деп аталады, себебі ағын блокталған және мониторды ала алмайды. Сондай-ақ codeтағы ағынның күйін білуге болады, бірақ бұл күйдің атауы JVisualVM шарттарымен сәйкес келмейді, бірақ олар ұқсас. Бұл жағдайда th1.getState()
цикл BLOCKEDfor
қайтарады , себебі Цикл жұмыс істеп тұрғанда, мониторды жіп басып алады , ал жіп бітеліп қалады және құлып қайтарылмайынша жұмысын жалғастыра алмайды. Синхрондау блоктарынан басқа, бүкіл әдісті синхрондауға болады. Мысалы, сыныптағы әдіс : lock
main
th1
HashTable
public synchronized int size() {
return count;
}
Бір уақыт бірлігінде бұл әдіс тек бір ағынмен орындалады. Бірақ бізге құлып керек, солай ма? Иә маған керек. Нысандық әдістер жағдайында құлып болады this
. Бұл тақырып бойынша қызықты пікірталас бар: " Синхрондалған блоктың орнына Синхрондалған әдісті пайдаланудың артықшылығы бар ма? ". Егер әдіс статикалық болса, онда құлып емес this
(өйткені статикалық әдіс үшін болуы мүмкін емес this
), сынып нысаны болады (Мысалы, Integer.class
).
Мониторда күтіңіз және күтіңіз. Хабарлау және барлық әдістерді хабарлау
Thread мониторға қосылған басқа күту әдісіне ие.sleep
және айырмашылығы join
, оны жай ғана шақыруға болмайды. Ал оның аты wait
. wait
Әдіс мониторында күткіміз келетін нысанда орындалады . Мысал көрейік:
public static void main(String []args) throws InterruptedException {
Object lock = new Object();
// task будет ждать, пока его не оповестят через lock
Runnable task = () -> {
synchronized(lock) {
try {
lock.wait();
} catch(InterruptedException e) {
System.out.println("interrupted");
}
}
// После оповещения нас мы будем ждать, пока сможем взять лок
System.out.println("thread");
};
Thread taskThread = new Thread(task);
taskThread.start();
// Ждём и после этого забираем себе лок, оповещаем и отдаём лок
Thread.currentThread().sleep(3000);
System.out.println("main");
synchronized(lock) {
lock.notify();
}
}
JVisualVM жүйесінде ол келесідей болады: Бұл қалай жұмыс істейтінін түсіну үшін әдістерге сілтеме жасайтынын есте сақтаңыз . Жіпке қатысты әдістердің болуы біртүрлі болып көрінеді . Бірақ жауап осында жатыр. Естеріңізде болса, Java тіліндегі әрбір нысанның тақырыбы бар. Тақырып әртүрлі қызмет ақпаратын, соның ішінде монитор туралы ақпаратты — құлыптау күйі туралы деректерді қамтиды. Естеріңізде болса, әрбір нысанда (яғни әрбір данада) ішкі құлып деп аталатын ішкі JVM нысанымен байланысы бар, оны монитор деп те атайды. Жоғарыдағы мысалда тапсырма мониторға байланыстырылған синхрондау блогына кіретінімізді сипаттайды . Егер бұл мониторда құлыпты алу мүмкін болса, онда . Бұл тапсырманы орындайтын ағын мониторды босатады , бірақ мониторда хабарландыруды күтіп тұрған ағындар кезегіне қосылады . Жіптердің бұл кезегі WAIT-SET деп аталады, ол мәнді дәлірек көрсетеді. Бұл кезектен гөрі жиынтық. Жіп тапсырма тапсырмасымен жаңа ағын жасайды, оны бастайды және 3 секунд күтеді. Бұл жоғары ықтималдықпен жаңа ағынға жіптен бұрын құлыпты ұстап алып , мониторда кезекке тұруға мүмкіндік береді. Осыдан кейін ағынның өзі синхрондау блогына кіреді және мониторда ағын туралы хабарламаны орындайды. Хабарландыру жіберілгеннен кейін ағын мониторды босатады және жаңа ағын (бұрын күтіп тұрған) монитордың шығарылуын күткеннен кейін орындалуын жалғастырады. Хабарландыруды тек бір ағынға ( ) немесе кезекте тұрған барлық ағындарға бірден жіберуге болады ( ). Толығырақ « Java тіліндегі notify() мен notifyAll() арасындағы айырмашылық » бөлімінен оқуға болады . Хабарландыру тәртібі JVM іске асыруға байланысты екенін ескеру маңызды. Толығырақ « Аштықты notify and notifyall көмегімен қалай шешуге болады? » бөлімінен оқи аласыз . Синхрондау нысанды көрсетпей-ақ орындалуы мүмкін. Бұл codeтың бөлек бөлімі емес, бүкіл әдіс синхрондалған кезде жасалуы мүмкін. Мысалы, статикалық әдістер үшін құлып сынып нысаны болады (арқылы алынған ): wait
notify
java.lang.Object
Object
lock
wait
lock
lock
main
main
main
lock
main
lock
lock
notify
notifyAll
.class
public static synchronized void printA() {
System.out.println("A");
}
public static void printB() {
synchronized(HelloWorld.class) {
System.out.println("B");
}
}
Құлыптарды пайдалану тұрғысынан екі әдіс бірдей. Егер әдіс статикалық болмаса, онда синхрондау токқа сәйкес орындалады instance
, яғни сәйкес this
. Айтпақшы, бұрын біз әдісті пайдалану арқылы getState
ағынның күйін алуға болатынын айтқанбыз. Міне, монитор кезекке қойған ағын, егер әдіс wait
күту уақытының шегін көрсетсе, күй КҮТУ немесе TIMED_WAITING болады.
Жіптің өмірлік циклі
Байқағанымыздай, ағым өмір ағымында өз мәртебесін өзгертеді. Негізінде бұл өзгерістер жіптің өмірлік циклі болып табылады. Жіп жаңа ғана жасалғанда, оның ЖАҢА күйі болады. Бұл позицияда ол әлі басталған жоқ және Java Thread Scheduler жаңа ағын туралы әлі ештеңе білмейді. Ағынды жоспарлаушы ағын туралы білуі үшінthread.start()
. Содан кейін ағын ЖҰМЫСТЫ күйге өтеді. Интернетте іске қосу және іске қосу күйлері бөлінген көптеген дұрыс емес схемалар бар. Бірақ бұл қате, өйткені... Java «іске қосуға дайын» және «іске қосу» күйлерін ажыратпайды. Жіп тірі, бірақ белсенді емес кезде (Runnable емес), ол екі күйдің бірінде болады:
- BLOCKED - қорғалған бөлімге кіруді күтеді, яғни. блокқа
synchonized
. - WAITING – шарт негізінде басқа ағынды күтеді. Шарт шын болса, ағынды жоспарлаушы ағынды бастайды.
getState
. isAlive
Сондай-ақ, ағындарда ағын аяқталмаса, ақиқат мәнін қайтаратын әдіс бар .
LockSupport және жіп тұрағы
Java 1.6 нұсқасынан бастап LockSupport деп аталатын қызықты механизм пайда болды . Бұл класс «рұқсат» немесе рұқсатты оны пайдаланатын әрбір ағынмен байланыстырады. Әдіс шақыруыpark
қоңырау кезінде сол рұқсатты алатын рұқсат қол жетімді болса, дереу қайтарады. Әйтпесе бұғатталған. Әдіске қоңырау шалу, unpark
ол әлі қол жетімді болмаса, рұқсатты қол жетімді етеді. Тек 1 рұқсат бар. Java API интерфейсінде LockSupport
белгілі Semaphore
. Қарапайым мысалды қарастырайық:
import java.util.concurrent.Semaphore;
public class HelloWorldApp{
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(0);
try {
semaphore.acquire();
} catch (InterruptedException e) {
// Просим разрешение и ждём, пока не получим его
e.printStackTrace();
}
System.out.println("Hello, World!");
}
}
Бұл code мәңгі күтеді, себебі семафорда қазір 0 рұқсаты бар. Кодпен шақырылған кезде acquire
(яғни, рұқсат сұрау), ағын рұқсат алғанша күтеді. Біз күтіп отырғандықтан, біз оны өңдеуге міндеттіміз InterruptedException
. Бір қызығы, семафор бөлек ағын күйін жүзеге асырады. JVisualVM-ге қарасақ, біздің күйіміз Wait емес, Парк екенін көреміз. Басқа мысалды қарастырайық:
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
//Запаркуем текущий поток
System.err.println("Will be Parked");
LockSupport.park();
// Как только нас распаркуют - начнём действовать
System.err.println("Unparked");
};
Thread th = new Thread(task);
th.start();
Thread.currentThread().sleep(2000);
System.err.println("Thread state: " + th.getState());
LockSupport.unpark(th);
Thread.currentThread().sleep(2000);
}
Жіп күйі КҮТІЛЕДІ болады, бірақ JVisualVM wait
бастап synchronized
және park
бастап арасындағы айырмашылықты көрсетеді LockSupport
. Бұл неге соншалықты маңызды LockSupport
? Java API интерфейсіне қайта оралайық және Thread State WAITING параметрін қарастырайық . Көріп отырғаныңыздай, оған кірудің тек үш жолы бар. 2 жол - бұл wait
және join
. Ал үшінші - LockSupport
. Java-дағы құлыптар бірдей принциптерге негізделген LockSupport
және жоғары деңгейлі құралдарды білдіреді. Біреуін қолданып көрейік. Мысалы, мынаны қарастырайық ReentrantLock
:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HelloWorld{
public static void main(String []args) throws InterruptedException {
Lock lock = new ReentrantLock();
Runnable task = () -> {
lock.lock();
System.out.println("Thread");
lock.unlock();
};
lock.lock();
Thread th = new Thread(task);
th.start();
System.out.println("main");
Thread.currentThread().sleep(2000);
lock.unlock();
}
}
Алдыңғы мысалдардағыдай, мұнда бәрі қарапайым. lock
біреудің ресурсты шығаруын күтеді. main
JVisualVM-ге қарасақ, жаңа ағын ағын оған құлыпты бергенше тұрақталатынын көреміз . Құлыптар туралы толығырақ мына жерден оқи аласыз: " Java 8-де көп ағынды бағдарламалау. Екінші бөлім. Өзгермелі нысандарға қол жеткізуді синхрондау " және " Java Lock API. Теория және қолдану мысалы ." Құлыптарды іске асыруды жақсырақ түсіну үшін « Phaser Class » шолуында Phazer туралы оқу пайдалы . Әртүрлі синхронизаторлар туралы айтатын болсақ, сіз Habré туралы « Java.util.concurrent.* Синхронизаторларға сілтеме » мақаласын оқуыңыз керек .
GO TO FULL VERSION