JavaRush /Java блогу /Random-KY /Javaдагы туюк жана аны менен күрөшүү ыкмалары
articles
Деңгээл

Javaдагы туюк жана аны менен күрөшүү ыкмалары

Группада жарыяланган
Көп жиптүү тиркемелерди иштеп чыгууда көбүнчө дилемма пайда болот: эң негизгиси тиркеменин ишенимдүүлүгү же натыйжалуулугу. Мисалы, биз жиптин коопсуздугу үчүн синхрондоштурууну колдонобуз жана синхрондоштуруу тартиби туура эмес болгон учурларда, биз туюкка учурашыбыз мүмкүн. Ресурстарды керектөөнү чектөө үчүн биз жип пулдарын жана семафорлорду да колдонобуз жана бул дизайндагы ката ресурстардын жетишсиздигинен улам туюкка алып келиши мүмкүн. Бул макалада биз туюктан кантип качуу керек, ошондой эле тиркеменин иштөөсүндөгү башка көйгөйлөр жөнүндө сүйлөшөбүз. Биз ошондой эле арызды туюк учурларда калыбына келтире тургандай кылып кантип жазууну карап чыгабыз. Javaдагы туюк жана аны менен күрөшүү ыкмалары - 1Туюктук – кээ бир ресурстарды ээлеген эки же андан көп процесстер башка процесстер ээлеген кээ бир башка ресурстарды алууга аракет кылып жаткан кырдаал жана процесстердин бири да өзүнө керектүү ресурсту ээлей албай, ошого жараша ээлеген ресурсту бошотпойт. Бул аныктама өтө жалпы жана ошондуктан түшүнүү кыйын; жакшыраак түшүнүү үчүн, биз мисалдарды колдонуу менен туюктун түрлөрүн карап чыгабыз.

Синхрондоштуруу тартиби Өз ара кулпу

Төмөнкү тапшырманы карап көрөлү: белгилүү бир суммадагы акчаны бир эсептен экинчисине которуу үчүн транзакцияны жүргүзгөн ыкманы жазуу керек. Чечим мындай көрүнүшү мүмкүн:
public void transferMoney(Account fromAccount, Account toAccount, Amount amount) throws InsufficientFundsException {
	synchronized (fromAccount) {
		synchronized (toAccount) {
			if (fromAccount.getBalance().compareTo(amount) < 0)
				throw new InsufficientFundsException();
			else {
				fromAccount.debit(amount);
				toAccount.credit(amount);
			}
		}
	}
}
Бир караганда, бул code нормалдуу түрдө шайкештештирилет; бизде баштапкы эсептин абалын текшерүү жана өзгөртүү жана көздөгөн эсепти өзгөртүү боюнча атомдук операция бар. Бирок, бул синхрондоштуруу стратегиясы менен туюк кырдаал пайда болушу мүмкүн. Келгиле, мунун кандай болорун бир мисал карап көрөлү. Эки транзакцияны жүргүзүү керек: А эсебинен В эсебине х акча которуу жана Б эсебинен А эсебине у акча которуу. Көбүнчө бул жагдай туңгуюкка алып келбейт, бирок, өкүнүчтүү жагдайларда 1-транзакция эсептин мониторун А, транзакция 2 эсептин мониторун В ээлейт. Натыйжада туюк болуп калат: транзакция 1 эсептин мониторунун бошотулушун күтөт. B, бирок транзакция 2 1-транзакция ээлеген А мониторуна кирүүгө тийиш. Туюктуктар менен байланышкан чоң көйгөйлөрдүн бири - аларды тестирлөөдө табуу оңой эмес. Мисалда сүрөттөлгөн кырдаалда да жиптер бөгөттөлбөй калышы мүмкүн, башкача айтканда, бул жагдай дайыма кайталанbyte, бул диагностиканы кыйла татаалдантат. Жалпысынан, детерминизмдин сүрөттөлгөн маселеси көп агым үчүн мүнөздүү (бирок бул аны жеңилдетпейт). Ошондуктан, codeду карап чыгуу көп жиптүү колдонмолордун сапатын жакшыртууда маанилүү ролду ойнойт, анткени ал тестирлөө учурунда кайра чыгаруу кыйын болгон каталарды аныктоого мүмкүндүк берет. Бул, албетте, колдонмону текшерүүнүн кереги жок дегенди билдирбейт; биз жөн гана codeду карап чыгууну унутпашыбыз керек. Бул code туюкка алып келбеши үчүн эмне кылышым керек? Бул бөгөттөө каттоо эсебин синхрондоштуруу башка тартипте болушу мүмкүн экенине байланыштуу. Демек, эгерде сиз эсептерге кандайдыр бир тартип киргизсеңиз (бул А эсеби В эсебине караганда азыраак деп айтууга мүмкүндүк берген кандайдыр бир эреже), анда көйгөй жоюлат. Муну кандай жасаш керек? Биринчиден, эгерде аккаунттарда кандайдыр бир уникалдуу идентификатор (мисалы, эсептин номери) сандык, кичине тамгалар же башка табигый тартип түшүнүгү (саптарды лексикографиялык тартипте салыштырууга болот) болсо, анда биз өзүбүздү бактылуу деп эсептей алабыз жана биз ар дайым Биз адегенде кичинекей эсептин мониторун, анан чоңун (же тескерисинче) ээлей алабыз.
private void doTransfer(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException {
	if (fromAcct.getBalance().compareTo(amount) < 0)
		throw new InsufficientFundsException();
	else {
		fromAcct.debit(amount);
		toAcct.credit(amount);
	}
}
public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException {
	int fromId= fromAcct.getId();
	int toId = fromAcct.getId();
	if (fromId < toId) {
		synchronized (fromAcct) {
			synchronized (toAcct) {
				doTransfer(fromAcct, toAcct, amount)}
			}
		}
	} else  {
		synchronized (toAcct) {
			synchronized (fromAcct) {
				doTransfer(fromAcct, toAcct, amount)}
			}
		}
	}
}
Экинчи вариант, эгерде бизде андай идентификатор жок болсо, аны өзүбүз ойлоп табышыбыз керек. Биринчи жакындоо үчүн биз an objectтерди хэш-code боюнча салыштыра алабыз. Кыязы, алар ар кандай болот. Бирок алар бирдей болуп чыксачы? Андан кийин синхрондоштуруу үчүн дагы бир an objectти кошууга туура келет. Бул бир аз татаал көрүнүшү мүмкүн, бирок эмне кыла аласыз? Мындан тышкары, үчүнчү an object өтө сейрек колдонулат. Натыйжа мындай болот:
private static final Object tieLock = new Object();
private void doTransfer(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException {
	if (fromAcct.getBalance().compareTo(amount) < 0)
		throw new InsufficientFundsException();
	else {
		fromAcct.debit(amount);
		toAcct.credit(amount);
	}
}
public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException {
	int fromHash = System.identityHashCode(fromAcct);
	int toHash = System.identityHashCode(toAcct);
	if (fromHash < toHash) {
		synchronized (fromAcct) {
			synchronized (toAcct) {
				doTransfer(fromAcct, toAcct, amount);
			}
		}
	} else if (fromHash > toHash) {
		synchronized (toAcct) {
			synchronized (fromAcct) {
				doTransfer(fromAcct, toAcct, amount);
			}
		}
	} else {
		synchronized (tieLock) {
			synchronized (fromAcct) {
				synchronized (toAcct) {
					doTransfer(fromAcct, toAcct, amount)
				}
			}
		}
	}
}

Объекттердин ортосундагы туюк

Сүрөттөлгөн бөгөттөө шарттары диагноз коюу үчүн туюктун эң оңой учурун билдирет. Көбүнчө көп жиптүү тиркемелерде ар кандай an objectтер бирдей синхрондоштурулган блокторго кирүүгө аракет кылышат. Бул туюктукка алып келиши мүмкүн. Төмөнкү мисалды карап көрөлү: учуу диспетчеринин колдонмосу. Учактар ​​диспетчерге көздөгөн жерине жеткенин айтып, конууга уруксат сурашат. Диспетчер анын багытында учкан учактар ​​тууралуу бардык маалыматты сактайт жана картада алардын абалын түзө алат.
class Plane {
	private Point location, destination;
	private final Dispatcher dispatcher;

	public Plane(Dispatcher dispatcher) {
		this.dispatcher = dispatcher;
	}
	public synchronized Point getLocation() {
		return location;
	}
	public synchronized void setLocation(Point location) {
		this.location = location;
		if (location.equals(destination))
		dispatcher.requestLanding(this);
	}
}

class Dispatcher {
	private final Set<Plane> planes;
	private final Set<Plane> planesPendingLanding;

	public Dispatcher() {
		planes = new HashSet<Plane>();
		planesPendingLanding = new HashSet<Plane>();
	}
	public synchronized void requestLanding(Plane plane) {
		planesPendingLanding.add(plane);
	}
	public synchronized Image getMap() {
		Image image = new Image();
		for (Plane plane : planes)
			image.drawMarker(plane.getLocation());
		return image;
	}
}
Бул codeдо туюкка алып келе турган ката бар экенин түшүнүү мурункуга караганда кыйыныраак. Бир караганда, ал кайра синхрондоштуруу жок, бирок бул андай эмес. setLocationСиз класстын Planeжана getMapкласстын методдору Dispatcherсинхрондоштурулганын жана башка класстардын синхрондуу ыкмаларын чакырганын байкаган чыгарсыз . Бул жалпысынан жаман практика. Муну кантип оңдоого болору кийинки бөлүмдө талкууланат. Натыйжада, учак турган жерге келип калса, кимдир бирөө картаны алууну чечкенде, туюк пайда болушу мүмкүн. Башкача айтканда, getMapжана методдору чакырылат, алар тиешелүү түрдө setLocationинстанцияны ээлейт . Андан кийин ыкма чакырат (айрыкча учурда бош эмес болгон инстанцияда ), ал ар бир инстанция үчүн монитордун бош болушун күтөт . Ошол эле учурда, ыкма деп аталат , ал эми инстанциянын монитору картаны тартуу менен алек болуп кала берет. Натыйжада туңгуюк болуп саналат. DispatcherPlanegetMapplane.getLocationPlanePlanesetLocationdispatcher.requestLandingDispatcher

Чалууларды ачуу

Мурунку бөлүмдө айтылгандай жагдайларды болтурбоо үчүн, башка an objectтердин ыкмаларына ачык чакырууларды колдонуу сунушталат. Башкача айтканда, синхрондоштурулган блоктун чегинен тышкары башка an objectтердин методдорун чакыруу. Эгерде ыкмалар ачык чалуу принцибинин негизинде кайра жазылса setLocation, getMapтуюк калуу мүмкүнчүлүгү жок кылынат. Ал, мисалы, мындай көрүнөт:
public void setLocation(Point location) {
	boolean reachedDestination;
	synchronized(this){
		this.location = location;
		reachedDestination = location.equals(destination);
	}
	if (reachedDestination)
		dispatcher.requestLanding(this);
}
………………………………………………………………………………
public Image getMap() {
	Set<Plane> copy;
	synchronized(this){
		copy = new HashSet<Plane>( planes);
	}
	Image image = new Image();
	for (Plane plane : copy)
		image.drawMarker(plane.getLocation());
	return image;
}

Ресурстук туюк

Бир эле учурда бир жип колдоно турган кээ бир ресурстарга кирүүгө аракет кылганда да туюктар пайда болушу мүмкүн. Мисал маалымат базасы байланыш бассейни болот. Кээ бир жиптер бир эле учурда эки туташууга кирүү керек болсо жана алар ар кандай тартипте кирсе, бул туюкка алып келиши мүмкүн. Негизинен, кулпулоонун бул түрү синхрондоштуруу тартибин кулпулоодон эч кандай айырмасы жок, ал кандайдыр бир codeду аткарууга аракет кылганда эмес, ресурстарга жетүү аракетинде пайда болот.

Кантип туюктан сактануу керек?

Албетте, эгер code эч кандай катасыз жазылса (мындай мисалдарды биз мурунку бөлүмдөрдө көрдүк), анда анда эч кандай туюктар болбойт. Бирок анын codeу катасыз жазылганына ким кепилдик бере алат? Албетте, тестирлөө каталардын олуттуу бөлүгүн аныктоого жардам берет, бирок биз мурда көргөндөй, көп агымдуу codeдогу каталарды диагностикалоо оңой эмес жана тестирлөөдөн кийин да туюк жагдайлар жок экенине ишене албайсыз. Биз кандайдыр бир жол менен өзүбүздү бөгөт коюудан коргой алабызбы? Жооп ооба. Окшош ыкмалар маалымат базасынын кыймылдаткычтарында колдонулат, алар көбүнчө туюктан чыгууга муктаж (базадагы транзакция механизми менен байланышкан). LockПакетте жеткorктүү интерфейс жана анын ишке ашырылышы java.util.concurrent.locksбул класстын инстанциясы менен байланышкан мониторду методду колдонууга аракет кылууга мүмкүндүк берет tryLock(эгерде мониторду ээлөө мүмкүн болсо, чындыкты кайтарат). Бизде интерфейсти ишке ашырган бир жуп an object бар дейли Lockжана биз алардын мониторлорун өз ара бөгөт коюудан качуу үчүн ээлешибиз керек. Сиз аны төмөнкүдөй ишке ашыра аласыз:
public void twoLocks(Lock A,  Lock B){
	while(true){
		if(A.tryLock()){
			if(B.tryLock())
			{
				try{
					//do something
				} finally{
					B.unlock();
					A.unlock();
				}
			} else{
				A.unlock();
			}
		}
	}
}
Бул программада көрүп тургандай, биз эки мониторду ээлеп, өз ара бөгөт коюу мүмкүнчүлүгүн жокко чыгарабыз. Көңүл буруңуз, блоктоо try- finallyзарыл, анткени пакеттеги класстар java.util.concurrent.locksмониторду автоматтык түрдө чыгарbyte жана эгер сиздин тапшырмаңызды аткаруу учурунда кандайдыр бир өзгөчөлүк пайда болсо, монитор кулпуланган абалда тыгылып калат. Туюктарды кантип аныктоого болот? JVM сизге туюктарды диагностикалоого, аларды жип таштандыларында көрсөтүүгө мүмкүндүк берет. Мындай таштандылар жиптин кандай абалда экендиги жөнүндө маалыматты камтыйт. Эгер ал бөгөттөлсө, таштанды жиптин чыгышын күтүп жаткан монитор жөнүндө маалыматты камтыйт. Жиптерди демпингден мурун, JVM күтүп жаткан (бош эмес) мониторлордун графигин карайт, ал эми циклдерди тапса, катышуучу мониторлорду жана жиптерди көрсөтүү менен туюк маалымат кошот. Туюк жиптердин таштандысы төмөнкүдөй көрүнөт:
Found one Java-level deadlock:
=============================
"ApplicationServerThread":
waiting to lock monitor 0x0f0d80cc (a MyDBConnection),
which is held by "ApplicationServerThread"
"ApplicationServerThread":
waiting to lock monitor 0x0f0d8fed (a MyDBCallableStatement),
which is held by "ApplicationServerThread"
Java stack information for the threads listed above:
"ApplicationServerThread":
at MyDBConnection.remove_statement
- waiting to lock <0x6f50f730> (a MyDBConnection)
at MyDBStatement.close
- locked <0x604ffbb0> (a MyDBCallableStatement)
...
"ApplicationServerThread":
at MyDBCallableStatement.sendBatch
- waiting to lock <0x604ffbb0> (a MyDBCallableStatement)
at MyDBConnection.commit
- locked <0x6f50f730> (a MyDBConnection)
Жогорудагы таштанды маалымат базасы менен иштеген эки жип бири-бирине бөгөт койгонун ачык көрсөтүп турат. Бул JVM өзгөчөлүгүн колдонуу менен туюктарды диагностикалоо үчүн, программанын ар кайсы жерлеринде жип таштоо операциясына чалууларды коюп, тиркемени сынап көрүү керек. Андан кийин, сиз алынган журналдарды талдоо керек. Эгерде алар туюкка учурагандыгын көрсөтсө, таштандыдан алынган маалымат анын кандай шартта болгонун аныктоого жардам берет. Жалпысынан алганда, сиз туюк мисалдардагыдай жагдайлардан качышыңыз керек. Мындай учурларда, колдонмо, кыязы, туруктуу иштейт. Бирок тестирлөө жана codeду карап чыгуу жөнүндө унутпаңыз. Бул көйгөйлөр пайда болгондо аныктоого жардам берет. Туюк талааны калыбына келтирүү өтө маанилүү болгон системаны иштеп жаткан учурларда, сиз «Туюктан кантип сактануу керек?» бөлүмүндө сүрөттөлгөн ыкманы колдонсоңуз болот. Бул учурда, пакеттеги lockInterruptiblyинтерфейс ыкмасы да пайдалуу болушу мүмкүн . Бул ыкманы колдонуу менен мониторду ээлеген жипти үзгүлтүккө учуратууга мүмкүндүк берет (ошондуктан мониторду бошотот). Lockjava.util.concurrent.locks
Комментарийлер
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION