스레드 상호 작용 기능에 대한 간략한 개요입니다. 이전에는 스레드가 서로 동기화되는 방식을 살펴보았습니다. 이번에는 스레드가 상호 작용할 때 발생할 수 있는 문제에 대해 알아보고 이를 방지할 수 있는 방법에 대해 이야기하겠습니다. 우리는 또한 더 깊은 연구를 위해 몇 가지 유용한 링크를 제공할 것입니다.
최고의 예는 " Java - Thread Starvation and Fairness "에서 찾을 수 있습니다. 이 예에서는 스레드가 Starvation에서 작동하는 방식과 Thread.sleep에서 Thread.wait로의 작은 변경 하나가 로드를 균등하게 분산할 수 있는 방법을 보여줍니다.
이 영상에서는 이에 대해 아무 것도 알려주지 않는 것이 더 나을 것 같습니다. 그럼 영상 링크만 남겨 놓겠습니다. " Java - 관계 발생 전 이해 "를 읽을 수 있습니다 .
소개
따라서 우리는 " Thread Can't Spoil Java: Part I - Threads " 리뷰에서 읽을 수 있는 Java에 스레드가 있다는 것과 스레드가 서로 동기화될 수 있다는 것을 알고 있습니다. 이에 대해서는 리뷰에서 다루었습니다. 스레드는 Java를 망칠 수 없습니다 ” 망치기: 파트 II - 동기화 ." 이제 스레드가 서로 어떻게 상호 작용하는지에 대해 이야기할 시간입니다. 공통 자원을 어떻게 공유합니까? 여기에 어떤 문제가 있을 수 있나요?이중 자물쇠
가장 심각한 문제는 Deadlock이다. 두 개 이상의 스레드가 서로를 영원히 기다리는 경우를 교착 상태라고 합니다. 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
그 반대의 경우도 마찬가지입니다. 블로킹은 해결불가이므로 Dead, 즉 dead입니다. 풀 수 없는 데스 그립과 탈출할 수 없는 데드 블록이 모두 존재합니다. 교착 상태 주제에 대해서는 " 교착 상태 - 동시성 #1 - 고급 Java " 비디오를 시청할 수 있습니다.
라이브락
Deadlock이 있으면 Livelock도 있습니까? 예, 있습니다) 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
...
예제에서 볼 수 있듯이 두 스레드는 두 잠금을 교대로 캡처하려고 시도하지만 실패합니다. 더욱이 그들은 교착 상태에 있지 않습니다. 즉, 시각적으로 모든 것이 괜찮고 작업을 수행하고 있습니다. JVisualVM에 따르면 휴면 기간과 파크 기간을 볼 수 있습니다(스레드가 잠금을 차지하려고 시도할 때, 앞서 스레드 동기화에 관해 논의할 때 논의한 것처럼 파크 상태로 전환됩니다 ). livelock 주제에 대해서는 " Java - Thread Livelock "이라는 예를 볼 수 있습니다.
굶주림
차단(교착 상태 및 라이브 잠금) 외에도 멀티스레딩 작업 시 기아 또는 "기아"라는 또 다른 문제가 있습니다. 이 현상은 스레드가 차단되지는 않지만 모든 사람을 위한 충분한 리소스가 없다는 점에서 차단과 다릅니다. 따라서 일부 스레드는 모든 실행 시간을 차지하지만 다른 스레드는 실행할 수 없습니다.https://www.logicbig.com/
경쟁 조건
멀티스레딩을 사용할 때 "경합 조건"이라는 것이 있습니다. 이 현상은 스레드가 서로 특정 리소스를 공유하고 이 경우 올바른 작동을 제공하지 않는 방식으로 코드가 작성된다는 사실에 있습니다. 예를 살펴보겠습니다: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에 알려줍니다. 이를 통해 모든 스레드의 실제 결과를 볼 수 있습니다. 이것은 매우 단순화된 공식입니다. 이 주제에 대해서는 " JSR 133(Java 메모리 모델) FAQvolatile
" 번역을 읽어보는 것이 좋습니다 . 또한 " Java 메모리 모델 " 및 " Java Volatile 키워드 " 자료에 대해 자세히 읽어 보시기 바랍니다 . 또한 이는 변경의 원자성에 관한 것이 아니라 가시성에 관한 것임을 기억하는 것이 중요합니다. "Race Condition"의 코드를 가져오면 IntelliJ Idea에서 힌트를 볼 수 있습니다. 이 검사(Inspection)는 2010년 릴리스 노트 에 나열된 문제 IDEA-61117 의 일부로 IntelliJ Idea에 추가되었습니다 .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
때때로 변경됩니다. 이 주제에 대한 간략한 개요가 있습니다. " Java의 원자 변수 소개 ". Atomic은 비교 및 교환 알고리즘을 기반으로 합니다. 이에 대한 자세한 내용은 Habré의 " JDK 7 및 8의 예를 사용하여 잠금 없는 알고리즘 비교 - CAS 및 FAA " 또는 Wikipedia의 " 교환과 비교 " 에 대한 기사에서 읽을 수 있습니다 .
http://jeremymanson.blogspot.com/2008/11/what-휘발성-means-in-java.html
GO TO FULL VERSION