JavaRush /Java блогы /Random-KK /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-транзакция A тіркелгі мониторын, 2-транзакция В шот мониторын алады. Нәтиже тығырыққа тіреледі: 1-транзакция шот мониторын босату үшін 2-транзакцияны күтеді. B, бірақ 2-транзакция 1-транзакциямен қамтылған А мониторына қол жеткізуі керек. Тұйықталуларға қатысты үлкен мәселелердің бірі - оларды тестілеу кезінде табу оңай емес. Мысалда сипатталған жағдайда да жіптер бітелмеуі мүмкін, яғни бұл жағдай үнемі қайталанбайды, бұл диагностиканы айтарлықтай қиындатады. Тұтастай алғанда, детерминизмнің сипатталған мәселесі көп ағынға тән (бірақ бұл оны жеңілдетпейді). Сондықтан codeты қарау көп ағынды қолданбалардың сапасын жақсартуда маңызды рөл атқарады, өйткені ол тестілеу кезінде қайта шығару қиын қателерді анықтауға мүмкіндік береді. Бұл, әрине, қолданбаны сынақтан өткізудің қажеті жоқ дегенді білдірмейді; біз codeты қарау туралы ұмытпауымыз керек. Бұл codeтың тығырықтан шығуына жол бермеу үшін не істеуім керек? Бұл бұғаттау есептік жазбаны синхрондаудың басқа тәртіпте орын алуына байланысты. Тиісінше, егер сіз шоттар бойынша қандай да бір тәртіпті енгізсеңіз (бұл A шоты В шотынан аз деп айтуға мүмкіндік беретін кейбір ереже), онда мәселе жойылады. Бұны қалай істейді? Біріншіден, егер тіркелгілерде қандай да бір бірегей идентификатор (мысалы, шот нөмірі) сандық, кіші әріппен немесе табиғи тәртіп тұжырымдамасы бар басқа біреу болса (жолдарды лексикографиялық тәртіпте салыстыруға болады), онда біз өзімізді бақытты деп санай аламыз және біз әрқашан Біз алдымен кішірек есептік жазбаның мониторын, содан кейін үлкенін (немесе керісінше) аламыз.
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 бойынша салыстыра аламыз. Сірә, олар әртүрлі болады. Бірақ олар бірдей болып шықса ше? Содан кейін синхрондау үшін басқа нысанды қосу керек болады. Бұл сәл күрделі көрінуі мүмкін, бірақ сіз не істей аласыз? Сонымен қатар, үшінші нысан өте сирек пайдаланылады. Нәтиже келесідей болады:
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)
				}
			}
		}
	}
}

Объектілер арасындағы тұйықталу

Сипатталған бұғаттау шарттары диагностикалау үшін ең оңай тұйықталу жағдайын көрсетеді. Көбінесе көп ағынды қолданбаларда әртүрлі нысандар бірдей синхрондалған блоктарға қол жеткізуге тырысады. Бұл тұйыққа тірелуі мүмкін. Келесі мысалды қарастырыңыз: рейс диспетчерінің қолданбасы. Ұшақтар диспетчерге межелі жерге жеткенін айтады және қонуға рұқсат сұрайды. Диспетчер өз бағытында ұшатын ұшақтар туралы барлық ақпаратты сақтайды және олардың орнын картаға сала алады.
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және әдістері шақырылады . Содан кейін әдіс шақырады (әсіресе қазір бос емес данада ), ол әрбір дана үшін монитордың бос болуын күтеді . Сонымен қатар, даналық монитор картаны сызумен бос емес болған кезде әдіс шақырылады . Нәтижесінде тығырыққа тіреледі. setLocationDispatcherPlanegetMapplane.getLocationPlanePlanesetLocationdispatcher.requestLandingDispatcher

Қоңырауларды ашу

Алдыңғы бөлімде сипатталғандай жағдайларды болдырмау үшін басқа нысандардың әдістеріне жалпы шақыруларды пайдалану ұсынылады. Яғни, синхрондалған блоктан тыс басқа 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Бумада қолжетімді интерфейс және оның іске асырулары java.util.concurrent.locksәдіс арқылы осы класс данасымен байланыстырылған мониторды басып көруге мүмкіндік береді tryLock(егер мониторды алу мүмкін болса, true мәнін қайтарады). Бізде интерфейсті жүзеге асыратын жұп нысандар бар делік 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мониторды автоматты түрде босатпайды және тапсырманы орындау кезінде қандай да бір ерекшелік орын алса, монитор құлыпталған күйде қалады. Тұйықталуларды қалай анықтауға болады? 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