Кратък преглед на подробностите за взаимодействието на нишките. Преди това разгледахме How нишките се синхронизират един с друг. Този път ще се потопим в проблемите, които могат да възникнат при взаимодействие на нишки, и ще говорим за това How да ги избегнем. Ще предоставим и някои полезни връзки за по-задълбочено проучване.
С инсталиран плъгин JVisualVM (чрез Инструменти -> Добавки), можем да видим къде е възникнала блокировката:
Според JVisualVM виждаме периоди на заспиване и период на паркиране (това е, когато нишка се опитва да получи заключване — тя влиза в състояние на паркиране, Howто обсъдихме по-рано, когато говорихме за синхронизация на нишки ) . Можете да видите пример за livelock тук: Java - Thread Livelock .
Можете да видите супер пример тук: Java - Thread Starvation and Fairness . Този пример показва Howво се случва с нишките по време на гладуване и How една малка промяна от
Тази проверка беше добавена към IntelliJ IDEA като част от проблем IDEA-61117 , който беше посочен в бележките по изданието през 2010 г.

Въведение
И така, знаем, че Java има нишки. Можете да прочетете за това в ревюто, озаглавено По-добре заедно: Java и класът Thread. Част I — Нишки на изпълнение . И ние проучихме факта, че нишките могат да се синхронизират една с друга в рецензията, озаглавена По-добре заедно: Java и класът Thread. Част II — Синхронизация . Време е да поговорим за това How нишките взаимодействат една с друга. Как споделят споделени ресурси? Какви проблеми могат да възникнат тук?
Безизходица
Най-страшният проблем от всички е задънената улица. Безизходица е, когато две or повече нишки вечно чакат другата. Ще вземем пример от уеб pageта на 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 bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s 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
: 
"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
, и обратно. Тази задънена улица е неразрешима и ние я наричаме задънена улица. Като смъртоносна хватка, която не може да бъде освободена, задънената улица е взаимно блокиране, което не може да бъде прекъснато. За друго обяснение на безизходното блокиране можете да гледате това видео: Обяснено блокиране и блокиране на живо .
Livelock
Ако има безизходица, има ли и livelock? Да, има :) Livelock се случва, когато нишките външно изглеждат живи, но не са в състояние да направят нищо, тъй като conditionто(ата), изисквани за тях, за да продължат работата си, не може да бъде изпълнено. По принцип livelock е подобен на deadlock, но нишките не "висят" в очакване на монитор. Вместо това те вечно правят нещо. Например:
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 зависи от реда, в който програмата за планиране на нишки на 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
...
Както можете да видите от примера, и двете нишки се опитват да придобият двете ключалки на свой ред, но не успяват. Но те не са в задънена улица. Външно всичко е наред и си вършат работата. 
Гладуване
В допълнение към блокиране и блокиране на живо, има друг проблем, който може да възникне по време на многопоточност: глад. Това явление се различава от предишните форми на блокиране по това, че нишките не са блокирани — те просто нямат достатъчно ресурси. В резултат на това, докато някои нишки отнемат цялото време за изпълнение, други не могат да се изпълняват:
https://www.logicbig.com/
Thread.sleep()
към Thread.wait()
ви позволява да разпределите натоварването равномерно. 
Състезателни условия
В многонишковостта има такова нещо като "състезание". Това явление се случва, когато нишките споделят ресурс, но 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 .
Летлив
Говорейки за взаимодействието на нишките,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
ключовата дума. Как и защо? Процесорът изпълнява всички действия. Но резултатите от изчисленията трябва да се съхраняват някъде. За това има основна памет и има кеш памет на процесора. Кешовете на процесора са като малка част от паметта, използвана за по-бърз достъп до данни, отколкото при достъп до основната памет. Но всичко има обратна страна: данните в кеша може да не са актуални (Howто в примера по-горе, когато стойността на полето за флаг не е актуализирана). Така чеvolatile
ключова дума казва на JVM, че не искаме да кешираме нашата променлива. Това позволява актуалният резултат да се вижда във всички теми. Това е силно опростено обяснение. Що се отнася до volatile
ключовата дума, горещо ви препоръчвам да прочетете тази статия . За повече информация също ви съветвам да прочетете Java Memory Model и Java Volatile Keyword . Освен това е важно да запомните, че volatile
става въпрос за видимостта, а не за атомарността на промените. Разглеждайки codeа в раздела „Условия на състезанието“, ще видим подсказка в IntelliJ IDEA: 
Атомност
Атомарните операции са операции, които не могат да бъдат разделени. Например операцията за присвояване на стойност на променлива трябва да бъде атомарна. За съжаление, операцията за увеличаване не е атомарна, тъй като увеличаването изисква до три операции на процесора: вземете старата стойност, добавете една към нея и след това запазете стойността. Защо атомарността е важна? С операцията за нарастване, ако има състояние на състезание, тогава споделеният ресурс (т.е. споделената стойност) може внезапно да се промени по всяко време. Освен това операциите, включващи 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());
}
}
Специалният AtomicInteger
клас винаги ще ни дава 30 000, но value
ще се променя от време на време. Има кратък преглед на тази тема: Въведение в атомарните променливи в Java . Алгоритъмът "сравняване и размяна" е в основата на атомните класове. Можете да прочетете повече за това тук в Сравнение на алгоритми без заключване - CAS и FAA на примера на JDK 7 и 8 or в статията Compare and swap в Wikipedia. 
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
Случва се-преди
Има една интересна и мистериозна концепция, наречена „случва се преди“. Като част от изучаването на нишките, трябва да прочетете за това. Връзката случва-преди показва реда, в който ще се виждат действията между нишките. Има много тълкувания и коментари. Ето една от най-новите презентации по тази тема: Връзки "Случва се преди" в Java .Резюме
В този преглед проучихме някои от спецификите на взаимодействието между нишките. Обсъдихме проблемите, които могат да възникнат, Howто и начините за тяхното идентифициране и отстраняване. Списък с допълнителни материали по темата:- Двойно проверено заключване
- ЧЗВ за JSR 133 (модел на паметта на Java).
- IQ 35: Как да предотвратим задънена улица?
- Концепции за паралелност в Java от Дъглас Хокинс (2017)
GO TO FULL VERSION