스레드가 상호 작용하는 방식에 대한 세부 사항에 대한 간략한 개요입니다. 이전에는 스레드가 서로 어떻게 동기화되는지 살펴보았습니다. 이번에는 스레드가 상호 작용할 때 발생할 수 있는 문제에 대해 자세히 살펴보고 이를 방지하는 방법에 대해 이야기하겠습니다. 또한 보다 심도 있는 연구를 위한 몇 가지 유용한 링크를 제공할 것입니다.
JVisualVM 플러그인이 설치되어 있으면(도구 -> 플러그인을 통해) 교착 상태가 발생한 위치를 확인할 수 있습니다.
JVisualVM에 따르면 휴면 기간과 유보 기간이 있습니다(이는 스레드가 잠금을 획득하려고 시도할 때입니다. 이전에 스레드 동기화에 대해 이야기할 때 논의한 것처럼 유휴 상태로 들어갑니다 ) . 여기에서 livelock의 예를 볼 수 있습니다: Java - Thread Livelock .
여기에서 훌륭한 예를 볼 수 있습니다: Java - Thread Starvation and Fairness .

소개
따라서 우리는 Java에 스레드가 있다는 것을 알고 있습니다. Better Together: Java and the Thread class 라는 제목의 리뷰에서 이에 대해 읽을 수 있습니다 . 파트 I — 실행 스레드 . 그리고 Better Together: Java and the Thread class라는 제목의 리뷰에서 스레드가 서로 동기화될 수 있다는 사실을 탐구했습니다 . 파트 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 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
, 그 반대. 이것은 교착 상태이며 해결할 수 없으며 교착 상태라고 합니다. 풀 수 없는 죽음의 손아귀처럼 교착 상태는 끊을 수 없는 상호 차단입니다. 교착 상태에 대한 또 다른 설명은 Deadlock and Livelock Explained 비디오를 시청할 수 있습니다 .
라이브록
교착 상태가 있으면 라이브 잠금도 있습니까? 예, 있습니다 :) Livelock은 스레드가 겉으로는 살아 있는 것처럼 보이지만 작업을 계속하는 데 필요한 조건이 충족될 수 없기 때문에 아무 것도 할 수 없을 때 발생합니다. 기본적으로 livelock은 교착 상태와 유사하지만 스레드가 모니터를 기다리면서 "멈추지" 않습니다. 대신 그들은 영원히 무언가를 하고 있습니다. 예를 들어:
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();
}
}
이 코드의 성공 여부는 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()
부하를 고르게 분산시키는 방법을 보여줍니다. 
경쟁 조건
멀티스레딩에는 "경합 조건"과 같은 것이 있습니다. 이 현상은 스레드가 리소스를 공유하지만 올바른 공유를 보장하지 않는 방식으로 코드가 작성되었을 때 발생합니다. 예를 살펴보십시오.
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();
}
}
이 코드는 처음에는 오류를 생성하지 않을 수 있습니다. 이 경우 다음과 같이 표시될 수 있습니다.
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
. 어떻게 그리고 왜? 프로세서는 모든 작업을 수행합니다. 그러나 계산 결과는 어딘가에 저장해야 합니다. 이를 위해 메인 메모리가 있고 프로세서의 캐시가 있습니다. 프로세서의 캐시는 메인 메모리에 액세스할 때보다 더 빠르게 데이터에 액세스하는 데 사용되는 작은 메모리 덩어리와 같습니다. 그러나 모든 것에는 단점이 있습니다. 캐시의 데이터가 최신이 아닐 수 있습니다(위의 예에서 플래그 필드의 값이 업데이트되지 않은 경우). 그래서volatile
키워드는 JVM에 변수를 캐시하고 싶지 않다고 알려줍니다. 이렇게 하면 모든 스레드에서 최신 결과를 볼 수 있습니다. 이것은 매우 단순화된 설명입니다. 키워드 에 관해서 는 이 기사를volatile
읽는 것이 좋습니다 . 자세한 내용은 Java Memory Model 및 Java Volatile Keyword를 참조하십시오 . 또한 변경 사항의 원자성이 아니라 가시성에 관한 것임을 기억하는 것이 중요합니다 . "경쟁 조건" 섹션의 코드를 보면 IntelliJ IDEA에 툴팁이 표시됩니다. 이 검사는 2010년 릴리스 노트 에 나열된 IDEA-61117 문제의 일부로 IntelliJ IDEA에 추가되었습니다 .volatile

원자성
원자성 작업은 나눌 수 없는 작업입니다. 예를 들어 변수에 값을 할당하는 작업은 원자적이어야 합니다. 불행하게도 증분 작업은 원자적이지 않습니다. 증분에는 세 가지 CPU 작업(이전 값 가져오기, 여기에 하나 추가, 값 저장)이 필요하기 때문입니다. 원자성이 중요한 이유는 무엇입니까? 증분 작업에서 경쟁 조건이 있는 경우 공유 리소스(즉, 공유 값)가 언제든지 갑자기 변경될 수 있습니다. 또한 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
수시로 변경됩니다. 이 주제에 대한 간략한 개요가 있습니다: Introduction to Atomic Variables in Java . "비교 및 교환" 알고리즘은 원자 클래스의 핵심입니다. 이에 대한 자세한 내용은 JDK 7 및 8의 예에 대한 잠금 해제 알고리즘 비교 - CAS 및 FAA 또는 Wikipedia의 비교 및 교환 기사에서 읽을 수 있습니다 .
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
GO TO FULL VERSION