JavaRush /Java Blog /Random-KO /스레드로 Java를 망칠 수는 없습니다: 3부 - 상호 작용
Viacheslav
레벨 3

스레드로 Java를 망칠 수는 없습니다: 3부 - 상호 작용

Random-KO 그룹에 게시되었습니다
스레드 상호 작용 기능에 대한 간략한 개요입니다. 이전에는 스레드가 서로 동기화되는 방식을 살펴보았습니다. 이번에는 스레드가 상호 작용할 때 발생할 수 있는 문제에 대해 알아보고 이를 방지할 수 있는 방법에 대해 이야기하겠습니다. 우리는 또한 더 깊은 연구를 위해 몇 가지 유용한 링크를 제공할 것입니다. 스레드로 Java를 망칠 수는 없습니다: 3부 - 상호 작용 - 1

소개

따라서 우리는 " 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. 스레드로 Java를 망칠 수는 없습니다: 파트 III - 상호 작용 - 2플러그인이 JVisualVM에 설치되어 있으면(도구 -> 플러그인을 통해) 교착 상태가 발생한 위치를 확인할 수 있습니다.
"Thread-1" - Thread t@12
   java.lang.Thread.State: BLOCKED
    at Deadlock$Friend.bowBack(Deadlock.java:16)
    - waiting to lock &lt33a78231> (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-1Livelock이 표시됩니다.
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
...
예제에서 볼 수 있듯이 두 스레드는 두 잠금을 교대로 캡처하려고 시도하지만 실패합니다. 더욱이 그들은 교착 상태에 있지 않습니다. 즉, 시각적으로 모든 것이 괜찮고 작업을 수행하고 있습니다. 스레드로 Java를 망칠 수는 없습니다: 파트 III - 상호 작용 - 3JVisualVM에 따르면 휴면 기간과 파크 기간을 볼 수 있습니다(스레드가 잠금을 차지하려고 시도할 때, 앞서 스레드 동기화에 관해 논의할 때 논의한 것처럼 파크 상태로 전환됩니다 ). livelock 주제에 대해서는 " Java - Thread Livelock "이라는 예를 볼 수 있습니다.

굶주림

차단(교착 상태 및 라이브 잠금) 외에도 멀티스레딩 작업 시 기아 또는 "기아"라는 또 다른 문제가 있습니다. 이 현상은 스레드가 차단되지는 않지만 모든 사람을 위한 충분한 리소스가 없다는 점에서 차단과 다릅니다. 따라서 일부 스레드는 모든 실행 시간을 차지하지만 다른 스레드는 실행할 수 없습니다. 스레드로 Java를 망칠 수는 없습니다: 파트 III - 상호 작용 - 4

https://www.logicbig.com/

최고의 예는 " Java - Thread Starvation and Fairness "에서 찾을 수 있습니다. 이 예에서는 스레드가 Starvation에서 작동하는 방식과 Thread.sleep에서 Thread.wait로의 작은 변경 하나가 로드를 균등하게 분산할 수 있는 방법을 보여줍니다. 스레드로 Java를 망칠 수는 없습니다: 3부 - 상호 작용 - 5

경쟁 조건

멀티스레딩을 사용할 때 "경합 조건"이라는 것이 있습니다. 이 현상은 스레드가 서로 특정 리소스를 공유하고 이 경우 올바른 작동을 제공하지 않는 방식으로 코드가 작성된다는 사실에 있습니다. 예를 살펴보겠습니다:
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스레드로 Java를 망칠 수는 없습니다: 파트 III - 상호 작용 - 6

원자성

원자적 연산은 나눌 수 없는 연산입니다. 예를 들어 변수에 값을 할당하는 작업은 원자적입니다. 불행하게도 증분은 원자적 연산이 아닙니다. 증분에는 최대 세 가지 작업이 필요합니다. 즉, 이전 값을 가져오고, 여기에 값을 추가하고, 값을 저장합니다. 원자성은 왜 중요한가? 증분 예에서는 경쟁 조건이 발생하면 언제든지 공유 리소스(즉, 공유 값)가 갑자기 변경될 수 있습니다. 또한 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의 " 교환과 비교 " 에 대한 기사에서 읽을 수 있습니다 . 스레드로 Java를 망칠 수는 없습니다: 3부 - 상호 작용 - 8

http://jeremymanson.blogspot.com/2008/11/what-휘발성-means-in-java.html

이전에 발생

흥미롭고 신비로운 일이 있습니다 - Happens Before. 흐름에 대해 말하면 그것에 대해 읽어 볼 가치가 있습니다. 발생 전 관계는 스레드 간의 작업이 표시되는 순서를 나타냅니다. 많은 해석과 해석이 있습니다. 이 주제에 대한 가장 최근 보고서 중 하나는 다음 보고서입니다.
이 영상에서는 이에 대해 아무 것도 알려주지 않는 것이 더 나을 것 같습니다. 그럼 영상 링크만 남겨 놓겠습니다. " Java - 관계 발생 전 이해 "를 읽을 수 있습니다 .

결과

이번 리뷰에서는 스레드 상호작용의 특징을 살펴보았습니다. 발생할 수 있는 문제와 이를 감지하고 제거하는 방법에 대해 논의했습니다. 주제에 대한 추가 자료 목록: #비아체슬라프
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION