CodeGym /Java Blog /무작위의 /더 나은 조합: Java와 Thread 클래스. 3부 — 상호 작용
John Squirrels
레벨 41
San Francisco

더 나은 조합: Java와 Thread 클래스. 3부 — 상호 작용

무작위의 그룹에 게시되었습니다
스레드가 상호 작용하는 방식에 대한 세부 사항에 대한 간략한 개요입니다. 이전에는 스레드가 서로 어떻게 동기화되는지 살펴보았습니다. 이번에는 스레드가 상호 작용할 때 발생할 수 있는 문제에 대해 자세히 살펴보고 이를 방지하는 방법에 대해 이야기하겠습니다. 또한 보다 심도 있는 연구를 위한 몇 가지 유용한 링크를 제공할 것입니다. 더 나은 조합: Java와 Thread 클래스.  3부 — 상호 작용 - 1

소개

따라서 우리는 Java에 스레드가 있다는 것을 알고 있습니다. Better Together: Java and the Thread class 라는 제목의 리뷰에서 이에 대해 읽을 수 있습니다 . 파트 I — 실행 스레드 . 그리고 Better Together: Java and the Thread class라는 제목의 리뷰에서 스레드가 서로 동기화될 수 있다는 사실을 탐구했습니다 . 파트 II — 동기화 . 스레드가 서로 상호 작용하는 방식에 대해 이야기할 시간입니다. 공유 리소스를 어떻게 공유합니까? 여기서 어떤 문제가 발생할 수 있습니까? 더 나은 조합: Java와 Thread 클래스.  3부 — 상호 작용 - 2

이중 자물쇠

가장 무서운 문제는 교착 상태입니다. 교착 상태는 둘 이상의 스레드가 서로를 영원히 기다리는 상태입니다. 교착 상태를 설명하는 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. 더 나은 조합: Java와 Thread 클래스.  3부 — 상호 작용 - 3JVisualVM 플러그인이 설치되어 있으면(도구 -> 플러그인을 통해) 교착 상태가 발생한 위치를 확인할 수 있습니다.

"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-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와 Thread 클래스.  3부 — 상호 작용 - 4JVisualVM에 따르면 휴면 기간과 유보 기간이 있습니다(이는 스레드가 잠금을 획득하려고 시도할 때입니다. 이전에 스레드 동기화에 대해 이야기할 때 논의한 것처럼 유휴 상태로 들어갑니다 ) . 여기에서 livelock의 예를 볼 수 있습니다: Java - Thread Livelock .

굶주림

교착 상태 및 라이브 잠금 외에도 멀티스레딩 중에 발생할 수 있는 또 다른 문제인 기아가 있습니다. 이 현상은 스레드가 차단되지 않는다는 점에서 이전 형태의 차단과 다릅니다. 단순히 리소스가 충분하지 않습니다. 결과적으로 일부 스레드는 모든 실행 시간을 사용하는 반면 다른 스레드는 실행할 수 없습니다. 더 나은 조합: Java와 Thread 클래스.  3부 — 상호 작용 - 5

https://www.logicbig.com/

여기에서 훌륭한 예를 볼 수 있습니다: Java - Thread Starvation and Fairness . Thread.sleep()이 예는 기아 상태에서 스레드에 어떤 일이 발생하는지, 그리고 에서 로의 작은 변경으로 Thread.wait()부하를 고르게 분산시키는 방법을 보여줍니다. 더 나은 조합: Java와 Thread 클래스.  3부 — 상호 작용 - 6

경쟁 조건

멀티스레딩에는 "경합 조건"과 같은 것이 있습니다. 이 현상은 스레드가 리소스를 공유하지만 올바른 공유를 보장하지 않는 방식으로 코드가 작성되었을 때 발생합니다. 예를 살펴보십시오.

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 ModelJava Volatile Keyword를 참조하십시오 . 또한 변경 사항의 원자성이 아니라 가시성에 관한 것임을 기억하는 것이 중요합니다 . "경쟁 조건" 섹션의 코드를 보면 IntelliJ IDEA에 툴팁이 표시됩니다. 이 검사는 2010년 릴리스 노트 에 나열된 IDEA-61117 문제의 일부로 IntelliJ IDEA에 추가되었습니다 .volatile더 나은 조합: Java와 Thread 클래스.  3부 — 상호 작용 - 7

원자성

원자성 작업은 나눌 수 없는 작업입니다. 예를 들어 변수에 값을 할당하는 작업은 원자적이어야 합니다. 불행하게도 증분 작업은 원자적이지 않습니다. 증분에는 세 가지 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의 비교 및 ​​교환 기사에서 읽을 수 있습니다 .더 나은 조합: Java와 Thread 클래스.  3부 — 상호 작용 - 9

http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html

이전에 발생

"이전에 일어난다"라는 흥미롭고 신비한 개념이 있습니다. 스레드 연구의 일환으로 스레드에 대해 읽어야 합니다. 발생 전 관계는 스레드 간의 작업이 표시되는 순서를 보여줍니다. 많은 해석과 논평이 있습니다. 다음은 이 주제에 대한 가장 최근의 프레젠테이션 중 하나입니다. Java "Happens-Before" Relationships .

요약

이 검토에서는 스레드가 상호 작용하는 방식에 대한 몇 가지 세부 사항을 살펴보았습니다. 발생할 수 있는 문제와 이를 식별하고 제거하는 방법에 대해 논의했습니다. 주제에 대한 추가 자료 목록: 더 나은 조합: Java와 Thread 클래스. 1부 — 실행 스레드 Java와 Thread 클래스를 함께 사용하면 더 좋습니다. 2부 — 동기화 함께 하면 더 좋습니다: Java와 Thread 클래스. 4부 — 호출 가능, 미래 및 친구 더 나은 조합: Java 및 Thread 클래스. 파트 V — Executor, ThreadPool, Fork/Join 더 나은 조합: Java 및 Thread 클래스. 6부 — 발사!
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION