Жиптин өз ара аракеттенүү өзгөчөлүктөрүнө кыскача сереп. Мурда биз жиптер бири-бири менен синхрондоштурууну карап чыктык. Бул жолу биз жиптер өз ара аракеттенгенде пайда болушу мүмкүн болгон көйгөйлөргө токтолуп, аларды кантип болтурбоо керектиги жөнүндө сүйлөшөбүз. Биз дагы тереңирээк изилдөө үчүн пайдалуу шилтемелерди беребиз.
Супер мисалды бул жерден тапса болот: " Java - Thread Starvation and Fairness ". Бул мисалда жиптер Ачарчылыкта кантип иштээрин жана Thread.sleepтен Thread.waitке бир кичинекей өзгөртүү кантип жүктү бирдей бөлүштүрө аларын көрсөтөт.
Бул видеодо бул тууралуу эч нерсе айтылбаганы жакшы. Андыктан мен жөн гана видеого шилтеме калтырам. Сиз окуй аласыз " Java - түшүнүү-байланыштарга чейин ".
Киришүү
Ошентип, биз Java'да жиптер бар экенин билебиз, алар жөнүндө " Java-ны буза алbyte: I бөлүк - Жиптер " жана жиптер бири-бири менен синхрондоштурууга болот, биз аларды карап чыгууда карап чыктык " Thread Can't Spoil Java ” Spoil: II Бөлүм - Синхрондоштуруу ." Жиптер бири-бири менен кандай байланышта экени жөнүндө сүйлөшүүгө убакыт келди. Алар жалпы ресурстарды кантип бөлүшөт? Бул менен кандай көйгөйлөр болушу мүмкүн?Туюк
Эң жаман көйгөй - бул туюк. Эки же андан көп жип бири-бирин түбөлүк күткөндө, бул туюк деп аталат. Келгиле, Oracle веб-сайтынан " Туйрук " концепциясын сүрөттөөдөн мисал алалы :public class Deadlock {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s has bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s has bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse = new Friend("Alphonse");
final Friend gaston = new Friend("Gaston");
new Thread(() -> alphonse.bow(gaston)).start();
new Thread(() -> gaston.bow(alphonse)).start();
}
}
Бул жерде туюк биринчи жолу пайда болбошу мүмкүн, бирок программаңыздын аткарылышы токтоп калса, анда иштөөгө убакыт келди jvisualvm
: JVisualVMде плагин орнотулган болсо (Куралдар -> Плагиндер аркылуу), биз туюктун кайсы жерде болгонун көрө алабыз:
"Thread-1" - Thread t@12
java.lang.Thread.State: BLOCKED
at Deadlock$Friend.bowBack(Deadlock.java:16)
- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
Жип 1 0 жиптен кулпуну күтүп жатат. Эмне үчүн мындай болуп жатат? Thread-1
аткарууну баштайт жана методду ишке ашырат Friend#bow
. Ал ачкыч сөз менен белгиленет synchronized
, башкача айтканда, биз мониторду this
. Методдун кире беришинде биз башкасына шилтеме алдык Friend
. Эми жип Thread-1
башка методду ишке ашыргысы келет Friend
, ошону менен андан кулпуну да алат. Бирок, эгерде башка жип (бул учурда Thread-0
) ыкмага кире алган болсо bow
, анда кулпу мурунтан эле бош эмес жана Thread-1
күтүп турат Thread-0
жана тескерисинче. Бөгөттөө чечилбейт, ошондуктан ал Өлгөн, башкача айтканда, өлгөн. Өлүм кармагычы да (бошотууга болбойт) жана адам качып кутула албай турган өлүк блок. Туюк темада сиз видеону көрө аласыз: " Туюктук - Кошумча №1 - Өркүндөтүлгөн Java ".
Livelock
Туюк болсо, анда Түйшүк барбы? Ооба, бар) Livelock бул жиптер сыртынан жандуу көрүнөт, бирок ошол эле учурда алар эч нерсе кыла алышпайт, анткени... оларыц ишини довам этдирмэге чалышян шертлери ерине етирип билмез. Түпкүлүгүндө, Livelock туюкка окшош, бирок жиптер мониторду күтүп жаткан системада “orп” калbyte, бирок ар дайым бир нерсе кылып жатышат. Мисалы:import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class App {
public static final String ANSI_BLUE = "\u001B[34m";
public static final String ANSI_PURPLE = "\u001B[35m";
public static void log(String text) {
String name = Thread.currentThread().getName(); //like Thread-1 or Thread-0
String color = ANSI_BLUE;
int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
if (val != 0) {
color = ANSI_PURPLE;
}
System.out.println(color + name + ": " + text + color);
try {
System.out.println(color + name + ": wait for " + val + " sec" + color);
Thread.currentThread().sleep(val * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Lock first = new ReentrantLock();
Lock second = new ReentrantLock();
Runnable locker = () -> {
boolean firstLocked = false;
boolean secondLocked = false;
try {
while (!firstLocked || !secondLocked) {
firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
log("First Locked: " + firstLocked);
secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
log("Second Locked: " + secondLocked);
}
first.unlock();
second.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(locker).start();
new Thread(locker).start();
}
}
Бул codeдун ийгorги Java жип пландоочусу жиптерди баштоо тартибинен көз каранды. Эгер биринчи башталса Thead-1
, биз Livelock алабыз:
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
Мисалдан көрүнүп тургандай, эки жип тең кезектешип эки кулпуну тең кармоого аракет кылат, бирок алар ийгorксиз болот. Анын үстүнө алар туңгуюкта эмес, башкача айтканда, визуалдык жактан аларда баары жакшы жана алар өз ишин аткарып жатышат. JVisualVM ылайык, биз уйку мезгилдерин жана парк мезгorн көрөбүз (бул жип кулпуну ээлөөгө аракет кылганда, ал сейилдөө абалына өтөт, биз жипти синхрондоштуруу жөнүндө мурда талкуулаганбыз ). Livelock темасында сиз мисалды көрө аласыз: " Java - Thread Livelock ".
Ачкачылык
Бөгөттөөдөн тышкары (туюк жана тирүү блок) көп агым менен иштөөдө дагы бир көйгөй бар - ачкачылык же "ачкалык". Бул көрүнүш бөгөттөөдөн жиптер бөгөттөлбөгөндүгү менен айырмаланат, бирок алар жөн гана бардыгына жетиштүү ресурстарга ээ эмес. Ошондуктан, кээ бир жиптер бүт аткаруу убактысын алат, ал эми башкалары аткарылbyte:https://www.logicbig.com/
Жарыш абалы
Multithreading менен иштөөдө "жарыш шарты" деген нерсе бар. Бул көрүнүш жиптер белгилүү бир ресурсту өз ара бөлүшүүдө жана code бул учурда туура иштөөнү камсыз кылбагандай кылып жазылганында. Келгиле, бир мисал карап көрөлү:public class App {
public static int value = 0;
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
int oldValue = value;
int newValue = ++value;
if (oldValue + 1 != newValue) {
throw new IllegalStateException(oldValue + " + 1 = " + newValue);
}
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
Бул code биринчи жолу ката жаратпашы мүмкүн. Жана мындай көрүнүшү мүмкүн:
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
at App.lambda$main$0(App.java:13)
at java.lang.Thread.run(Thread.java:745)
Көрүнүп тургандай, ал дайындалып жатканда, newValue
бир нерсе туура эмес болуп, newValue
дагы көп болду. Жарыш абалындагы жиптердин айрымдары value
бул эки команданын ортосунда өзгөрүүгө жетишти. Көрүнүп тургандай, жиптердин ортосунда жарыш пайда болду. Эми акча транзакцияларында ушундай каталарды кетирбөө канчалык маанилүү экенин элестетип көрүңүз... Мисалдарды жана диаграммаларды бул жерден тапса болот: “ Java жипинде жарыш абалын симуляциялоо үчүн code ”.
Учуучу
Жиптердин өз ара аракеттенүүсү жөнүндө сөз кылып жатып, өзгөчө ачкыч сөздү белгилей кетүү керекvolatile
. Келгиле, жөнөкөй мисалды карап көрөлү:
public class App {
public static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Runnable whileFlagFalse = () -> {
while(!flag) {
}
System.out.println("Flag is now TRUE");
};
new Thread(whileFlagFalse).start();
Thread.sleep(1000);
flag = true;
}
}
Эң кызыгы, жогорку ыктымалдуулук менен ал иштебей калат. Жаңы жип өзгөрүүнү көрбөйт flag
. Муну оңдоо үчүн, талаа үчүн flag
ачкыч сөздү көрсөтүшүңүз керек volatile
. Кантип жана эмнеге? Бардык аракеттер процессор тарабынан аткарылат. Бирок эсептөө натыйжалары бир жерде сакталышы керек. Бул үчүн процессордо негизги эс жана аппараттык кэш бар. Бул процессордун кэштери негизги эстутумга караганда тезирээк маалыматтарга жетүү үчүн эстутумдун кичинекей бөлүгү сыяктуу. Бирок бардыгынын терс жагы да бар: кэштеги маалыматтар учурдагы болбошу мүмкүн (жогорку мисалдагыдай, желектин мааниси жаңыртылган эмес). Ошентип, ачкыч сөз volatile
JVMге биз өзгөрмөбүздү кэштегибиз келбейт деп айтат. Бул бардык жиптерде чыныгы натыйжаны көрүүгө мүмкүндүк берет. Бул абдан жөнөкөйлөштүрүлгөн формула болуп саналат. Бул темада " JSR 133 (Java Memory Model) FAQvolatile
" котормосун окуу сунушталат . Мен ошондой эле “ Java Memory Model ” жана “ Java Volatile Keyword ” материалдары жөнүндө көбүрөөк окууну сунуштайм . Кошумчалай кетсек, бул өзгөрүүлөрдүн атомдуулугу жөнүндө эмес, көрүнүү жөнүндө экенин эстен чыгарбоо керек . Эгер биз "Жарыш шартынан" codeду алсак, IntelliJ Ideaдан кыйытты көрөбүз: Бул текшерүү (Текшерүү) IntelliJ Ideaга IDEA-61117 чыгарылышынын бир бөлүгү катары кошулган , ал 2010-жылы Release Notes тизмесинде көрсөтүлгөн .volatile
Атомдук
Атомдук операциялар бөлүүгө болбой турган операциялар. Мисалы, өзгөрмөгө маани берүү операциясы атомдук. Тилекке каршы, көбөйтүү атомдук операция эмес, анткени өсүү үч операцияны талап кылат: эски маанини алуу, ага бирди кошуу жана маанини сактоо. Эмне үчүн атомдук маанилүү? Көбөйтүү мисалында, эгерде жарыш шарты пайда болсо, каалаган убакта бөлүшүлгөн ресурс (б.а., жалпы маани) күтүлбөгөн жерден өзгөрүшү мүмкүн. Мындан тышкары, 64-бит структуралар да атомдук эмес, маанилүү, мисалыlong
жана double
. Көбүрөөк бул жерден окуй аласыз: " 64 биттик маанилерди окуп жана жазганда атомдуулукту камсыз кылыңыз ". Атомдук көйгөйлөрдүн мисалын төмөнкү мисалдан көрүүгө болот:
public class App {
public static int value = 0;
public static AtomicInteger atomic = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
value++;
atomic.incrementAndGet();
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
Thread.sleep(300);
System.out.println(value);
System.out.println(atomic.get());
}
}
Атом менен иштөө үчүн атайын класс Integer
бизге дайыма 30000 көрсөтөт, бирок value
ал мезгил-мезгor менен өзгөрүп турат. Бул тема боюнча кыскача сереп бар " Javaдагы атомдук өзгөрмөлөргө киришүү ". Atomic Салыштыруу жана алмаштыруу алгоритмине негизделген. Бул тууралуу Habré макаласында " Блоксуз алгоритмдерди салыштыруу - CAS жана FAA JDK 7 жана 8 мисалында " же Wikipediaдагы " Алмашуу менен салыштыруу " жөнүндө макаладан окуй аласыз.
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
Буга чейин болот
Кызыктуу жана сырдуу нерсе бар - Мурда болот. Агымдар жөнүндө сөз кыла турган болсок, ал жөнүндө окуу да арзырлык. Мурда болгон мамилеси жиптердин ортосундагы аракеттер көрүнө турган тартипти көрсөтөт. Көптөгөн чечмелөөлөр жана чечмелөөлөр бар. Бул тема боюнча акыркы отчеттордун бири бул отчет болуп саналат:Жыйынтыктар
Бул кароодо биз жип менен өз ара аракеттенүүнүн өзгөчөлүктөрүн карадык. Биз пайда болушу мүмкүн болгон көйгөйлөрдү жана аларды аныктоо жана жоюунун жолдорун талкууладык. Тема боюнча кошумча материалдардын тизмеси:- Дагы бир жолу эки жолу текшерилген кулпулоо жөнүндө
- JSR 133 (Java Memory Model) FAQ (которуу)
- Өркүндөтүлгөн Java - конкуренция (Юрий Ткач)
- Дуглас Хоукинс (2017) Java тorндеги конкуренция концепциялары
GO TO FULL VERSION