CodeGym /Java Blog /무작위의 /더 나은 조합: Java와 Thread 클래스. 2부 — 동기화
John Squirrels
레벨 41
San Francisco

더 나은 조합: Java와 Thread 클래스. 2부 — 동기화

무작위의 그룹에 게시되었습니다

소개

따라서 우리는 Java에 스레드가 있다는 것을 알고 있습니다. Better Together: Java and the Thread class 라는 제목의 리뷰에서 이에 대해 읽을 수 있습니다 . 파트 I — 실행 스레드 . 작업을 병렬로 수행하려면 스레드가 필요합니다. 이로 인해 스레드가 어떻게든 서로 상호 작용할 가능성이 높습니다. 이것이 어떻게 발생하고 어떤 기본 도구가 있는지 살펴보겠습니다. 더 나은 조합: Java와 Thread 클래스.  파트 II — 동기화 - 1

생산하다

Thread.yield() 는 당혹스럽고 거의 사용되지 않습니다. 인터넷에서 다양한 방식으로 설명됩니다. 스레드 우선 순위에 따라 스레드가 내려갈 스레드 대기열이 있다고 쓰는 일부 사람들을 포함합니다. 다른 사람들은 스레드가 "실행 중"에서 "실행 가능"으로 상태를 변경할 것이라고 기록합니다(이러한 상태 간에 구분이 없음에도 불구하고, 즉 Java는 이를 구분하지 않습니다). 현실은 훨씬 덜 알려져 있지만 어떤 의미에서는 더 단순하다는 것입니다. 메서드 설명서 에 더 나은 조합: Java와 Thread 클래스.  2부 - 동기화 - 2버그( JDK-6416721: (spec thread) Fix Thread.yield() javadoc )가 기록되어 있습니다 . yield()읽어보면 분명하다.yield()메소드는 실제로 Java 스레드 스케줄러에 이 스레드에 더 적은 실행 시간이 주어질 수 있다는 몇 가지 권장 사항만 제공합니다. 그러나 실제로 발생하는 것, 즉 스케줄러가 권장 사항에 따라 작동하는지 여부와 일반적으로 수행하는 작업은 JVM의 구현 및 운영 체제에 따라 다릅니다. 그리고 그것은 다른 요인들에 따라서도 달라질 수 있습니다. 모든 혼란은 Java 언어가 발전함에 따라 멀티스레딩이 다시 생각되었기 때문일 가능성이 큽니다. 자세한 내용은 Java Thread.yield() 소개: 개요를 참조 하십시오 .

스레드는 실행 중에 휴면 상태가 될 수 있습니다. 이것은 다른 스레드와 상호 작용하는 가장 쉬운 유형입니다. Java 코드를 실행하는 Java 가상 머신을 실행하는 운영 체제에는 자체 스레드 스케줄러가 있습니다 . 시작할 스레드와 시기를 결정합니다. 프로그래머는 JVM을 통해서만 Java 코드에서 직접 이 스케줄러와 상호 작용할 수 없습니다. 그 또는 그녀는 스케줄러에게 잠시 동안 스레드를 일시 중지하도록 요청할 수 있습니다. 자세한 내용은 Thread.sleep()멀티스레딩 작동 방식 문서에서 확인할 수 있습니다 . Windows 운영 체제에서 스레드가 작동하는 방식을 확인할 수도 있습니다. Internals of Windows Thread . 그리고 이제 직접 눈으로 확인해보자. 다음 코드를 다음 이름의 파일에 저장합니다 HelloWorldApp.java.

class HelloWorldApp {
    public static void main(String []args) {
        Runnable task = () -> {
            try {
                int secToWait = 1000 * 60;
                Thread.currentThread().sleep(secToWait);
                System.out.println("Woke up");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(task);
        thread.start();
    }
}
보시다시피 프로그램이 종료된 후 60초 동안 대기하는 작업이 있습니다. " " 명령을 사용하여 컴파일한 javac HelloWorldApp.java다음 " java HelloWorldApp"을 사용하여 프로그램을 실행합니다. 별도의 창에서 프로그램을 시작하는 것이 가장 좋습니다. 예를 들어, Windows에서는 다음과 같습니다. start java HelloWorldApp. 우리는 jps 명령을 사용하여 PID(프로세스 ID)를 가져오고 "로 스레드 목록을 엽니다 jvisualvm --openpid pid. 더 나은 조합: Java와 Thread 클래스.  2부 — 동기화 - 3보시다시피 스레드는 이제 "Sleeping" 상태입니다. 사실 더 우아한 방법이 있습니다. 우리 스레드는 달콤한 꿈을 꿉니다:

try {
	TimeUnit.SECONDS.sleep(60);
	System.out.println("Woke up");
} catch (InterruptedException e) {
	e.printStackTrace();
}
우리가 모든 곳에서 처리하고 있다는 것을 알고 계셨습니까 InterruptedException? 이유를 이해합시다.

스레드.인터럽트()

문제는 스레드가 대기/수면 중인 동안 누군가 인터럽트를 원할 수 있다는 것입니다. 이 경우 InterruptedException. 이 메커니즘은 메서드가 더 이상 사용되지 않음, 즉 구식이고 바람직하지 않은 것으로 선언된 후에 만들어졌습니다 Thread.stop(). 그 이유는 stop()메서드가 호출될 때 스레드가 단순히 "종료"되어 매우 예측할 수 없었기 때문입니다. 스레드가 언제 중지될지 알 수 없고 데이터 일관성을 보장할 수 없습니다. 스레드가 종료되는 동안 파일에 데이터를 쓰고 있다고 상상해 보십시오. 스레드를 죽이는 것보다 Java의 제작자는 중단되어야 한다고 말하는 것이 더 논리적이라고 결정했습니다. 이 정보에 어떻게 대응하느냐는 스레드 자체가 결정할 문제다. 자세한 내용은 Thread.stop이 더 이상 사용되지 않는 이유를 읽어보세요.오라클 웹사이트에서. 예를 살펴보겠습니다.

public static void main(String []args) {
	Runnable task = () -> {
		try {
			TimeUnit.SECONDS.sleep(60);
		} catch (InterruptedException e) {
			System.out.println("Interrupted");
		}
	};
	Thread thread = new Thread(task);
	thread.start();
	thread.interrupt();
}
이 예에서는 60초를 기다리지 않습니다. 대신 "Interrupted"를 즉시 표시합니다. interrupt()스레드에서 메서드를 호출했기 때문입니다 . 이 메서드는 "인터럽트 상태"라는 내부 플래그를 설정합니다. 즉, 각 스레드에는 직접 액세스할 수 없는 내부 플래그가 있습니다. 그러나 우리는 이 플래그와 상호작용하기 위한 기본 방법을 가지고 있습니다. 그러나 그것이 유일한 방법은 아닙니다. 스레드는 무언가를 기다리지 않고 단순히 작업을 수행하면서 실행 중일 수 있습니다. 그러나 다른 사람들이 특정 시간에 작업을 끝내기를 원할 것이라고 예상할 수 있습니다. 예를 들어:

public static void main(String []args) {
	Runnable task = () -> {
		while(!Thread.currentThread().isInterrupted()) {
			// Do some work
		}
		System.out.println("Finished");
	};
	Thread thread = new Thread(task);
	thread.start();
	thread.interrupt();
}
위의 예에서 while루프는 스레드가 외부에서 중단될 때까지 실행됩니다. 플래그 에 관해서는 isInterrupted우리가 를 잡으면 InterruptedExceptionisInterrupted 플래그가 재설정되고 isInterrupted()false를 반환한다는 것을 아는 것이 중요합니다. Thread 클래스 에는 현재 스레드에만 적용되는 정적 Thread.interrupted() 메서드도 있지만 이 메서드는 플래그를 false로 재설정합니다! 스레드 중단 이라는 제목의 이 장에서 자세한 내용을 읽으십시오 .

조인(다른 스레드가 완료될 때까지 대기)

가장 간단한 유형의 대기는 다른 스레드가 완료되기를 기다리는 것입니다.

public static void main(String []args) throws InterruptedException {
	Runnable task = () -> {
		try {
			TimeUnit.SECONDS.sleep(5);
		} catch (InterruptedException e) {
			System.out.println("Interrupted");
		}
	};
	Thread thread = new Thread(task);
	thread.start();
	thread.join();
	System.out.println("Finished");
}
이 예에서 새 스레드는 5초 동안 휴면 상태가 됩니다. 동시에 주 스레드는 잠자는 스레드가 깨어나 작업을 마칠 때까지 기다립니다. JVisualVM에서 스레드의 상태를 보면 다음과 같이 표시됩니다. 더 나은 조합: Java와 Thread 클래스.  2부 - 동기화 - 4모니터링 도구 덕분에 스레드에서 진행 중인 작업을 볼 수 있습니다. 메서드 는 호출된 스레드가 살아있는 한 join실행되는 Java 코드가 있는 메서드이기 때문에 매우 간단합니다 . wait()스레드가 죽자마자(작업이 끝나면) 대기가 중단됩니다. 그리고 그것이 방법의 모든 마법입니다 join(). 이제 가장 흥미로운 것으로 넘어 갑시다.

감시 장치

멀티스레딩에는 모니터 개념이 포함됩니다. 모니터라는 단어는 16세기 라틴어를 통해 영어로 왔으며 "과정을 관찰, 확인 또는 지속적으로 기록하는 데 사용되는 도구 또는 장치"를 의미합니다. 이 기사의 맥락에서 기본 사항을 다루려고 노력할 것입니다. 자세한 내용을 원하는 사람은 링크된 자료를 참조하십시오. JLS(Java Language Specification): 17.1로 여정을 시작합니다. 동기화 . 다음과 같습니다. 더 나은 조합: Java와 Thread 클래스.  2부 — 동기화 - 5Java는 스레드 간 동기화를 위해 "모니터" 메커니즘을 사용합니다. 모니터는 각 개체와 연결되며 스레드는 를 사용하여 모니터를 획득 lock()하거나 해제 할 수 있습니다 unlock(). 다음으로 Oracle 웹 사이트에서 자습서 Intrinsic Locks and Synchronization을 찾을 수 있습니다.. 이 자습서에서는 Java의 동기화가 고유 잠금 또는 모니터 잠금 이라는 내부 엔터티를 중심으로 구축된다고 말합니다 . 이 잠금 장치는 종종 단순히 " 모니터 "라고 합니다. 우리는 또한 Java의 모든 개체에 고유한 잠금이 연결되어 있음을 다시 확인합니다. Java-Intrinsic Locks and Synchronization을 읽을 수 있습니다 . 다음으로 Java의 개체를 모니터와 연결하는 방법을 이해하는 것이 중요합니다. Java에서 각 개체에는 프로그래머가 코드에서 사용할 수 없지만 가상 머신이 개체와 올바르게 작동하는 데 필요한 내부 메타데이터를 저장하는 헤더가 있습니다. 개체 헤더에는 다음과 같은 "표시 단어"가 포함되어 있습니다. 더 나은 조합: Java와 Thread 클래스.  2부 — 동기화 - 6

https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf

다음은 매우 유용한 JavaWorld 기사입니다. Java 가상 머신이 스레드 동기화를 수행하는 방법 . 이 문서는 JDK 버그 추적 시스템 JDK-8183909 문제의 "요약" 섹션에 있는 설명과 결합되어야 합니다 . JEP-8183909 에서 같은 내용을 읽을 수 있습니다 . 따라서 Java에서 모니터는 개체와 연결되며 스레드가 잠금을 획득(또는 가져오기)하려고 할 때 스레드를 차단하는 데 사용됩니다. 가장 간단한 예는 다음과 같습니다.

public class HelloWorld{
    public static void main(String []args){
        Object object = new Object();
        synchronized(object) {
            System.out.println("Hello World");
        }
    }
}
여기서 현재 스레드(이러한 코드 행이 실행되는 스레드)는 키워드를 사용 synchronized하여object"\잠금을 가져오거나 획득하기 위한 변수입니다. 아무도 모니터에 대해 경합하지 않는 경우(즉, 동일한 개체를 사용하여 동기화된 코드를 실행하는 사람이 아무도 없는 경우) Java는 "편향된 잠금"이라는 최적화를 수행하려고 시도할 수 있습니다. 해당 태그와 스레드가 모니터의 잠금을 소유하는 레코드에 대한 레코드가 개체 헤더의 마크 워드에 추가됩니다. 이렇게 하면 모니터를 잠그는 데 필요한 오버헤드가 줄어듭니다. 모니터가 이전에 다른 스레드에 의해 소유된 경우 이러한 잠금으로는 충분하지 않습니다. JVM은 다음 잠금 유형인 "기본 잠금"으로 전환합니다. CAS(비교 및 교환) 작업을 사용합니다. 또한 객체 헤더의 마크 워드 자체는 더 이상 마크 워드를 저장하지 않고 마크 워드가 저장된 위치에 대한 참조를 저장하며 JVM이 기본 잠금을 사용하고 있음을 이해할 수 있도록 태그가 변경됩니다. 여러 스레드가 모니터에 대해 경쟁(경합)하는 경우(하나는 잠금을 획득하고 두 번째 스레드는 잠금이 해제되기를 기다리고 있음) 마크 워드의 태그가 변경되고 마크 워드는 이제 모니터에 대한 참조를 저장합니다. 개체로 — JVM의 일부 내부 엔터티입니다. JDK Enchancement Proposal(JEP)에 명시된 바와 같이 이 상황에서는 이 엔터티를 저장하기 위해 메모리의 기본 힙 영역에 공간이 필요합니다. 이 내부 엔터티의 메모리 위치에 대한 참조는 개체 헤더의 마크 워드에 저장됩니다. 따라서 모니터는 실제로 여러 스레드 간에 공유 리소스에 대한 액세스를 동기화하기 위한 메커니즘입니다. JVM은 이 메커니즘의 여러 구현 간에 전환합니다. 따라서 간단하게 모니터에 대해 이야기할 때 실제로는 잠금에 대해 이야기하고 있습니다. 두 번째는 잠금이 해제되기를 기다리고 있습니다.) 그런 다음 마크 워드의 태그가 변경되고 마크 워드는 이제 모니터에 대한 참조를 개체(JVM의 일부 내부 엔터티)로 저장합니다. JDK Enchancement Proposal(JEP)에 명시된 바와 같이 이 상황에서는 이 엔터티를 저장하기 위해 메모리의 기본 힙 영역에 공간이 필요합니다. 이 내부 엔터티의 메모리 위치에 대한 참조는 개체 헤더의 마크 워드에 저장됩니다. 따라서 모니터는 실제로 여러 스레드 간에 공유 리소스에 대한 액세스를 동기화하기 위한 메커니즘입니다. JVM은 이 메커니즘의 여러 구현 간에 전환합니다. 따라서 간단하게 모니터에 대해 이야기할 때 실제로는 잠금에 대해 이야기하고 있습니다. 두 번째는 잠금이 해제되기를 기다리고 있습니다.) 그런 다음 마크 워드의 태그가 변경되고 마크 워드는 이제 모니터에 대한 참조를 개체(JVM의 일부 내부 엔터티)로 저장합니다. JDK Enchancement Proposal(JEP)에 명시된 바와 같이 이 상황에서는 이 엔터티를 저장하기 위해 메모리의 기본 힙 영역에 공간이 필요합니다. 이 내부 엔터티의 메모리 위치에 대한 참조는 개체 헤더의 마크 워드에 저장됩니다. 따라서 모니터는 실제로 여러 스레드 간에 공유 리소스에 대한 액세스를 동기화하기 위한 메커니즘입니다. JVM은 이 메커니즘의 여러 구현 간에 전환합니다. 따라서 간단하게 모니터에 대해 이야기할 때 실제로는 잠금에 대해 이야기하고 있습니다. 마크 워드는 이제 모니터에 대한 참조를 개체(JVM의 일부 내부 엔터티)로 저장합니다. JDK Enchancement Proposal(JEP)에 명시된 바와 같이 이 상황에서는 이 엔터티를 저장하기 위해 메모리의 기본 힙 영역에 공간이 필요합니다. 이 내부 엔터티의 메모리 위치에 대한 참조는 개체 헤더의 마크 워드에 저장됩니다. 따라서 모니터는 실제로 여러 스레드 간에 공유 리소스에 대한 액세스를 동기화하기 위한 메커니즘입니다. JVM은 이 메커니즘의 여러 구현 간에 전환합니다. 따라서 간단하게 모니터에 대해 이야기할 때 실제로는 잠금에 대해 이야기하고 있습니다. 마크 워드는 이제 모니터에 대한 참조를 개체(JVM의 일부 내부 엔터티)로 저장합니다. JDK Enchancement Proposal(JEP)에 명시된 바와 같이 이 상황에서는 이 엔터티를 저장하기 위해 메모리의 기본 힙 영역에 공간이 필요합니다. 이 내부 엔터티의 메모리 위치에 대한 참조는 개체 헤더의 마크 워드에 저장됩니다. 따라서 모니터는 실제로 여러 스레드 간에 공유 리소스에 대한 액세스를 동기화하기 위한 메커니즘입니다. JVM은 이 메커니즘의 여러 구현 간에 전환합니다. 따라서 간단하게 모니터에 대해 이야기할 때 실제로는 잠금에 대해 이야기하고 있습니다. 이 내부 엔터티의 메모리 위치에 대한 참조는 개체 헤더의 마크 워드에 저장됩니다. 따라서 모니터는 실제로 여러 스레드 간에 공유 리소스에 대한 액세스를 동기화하기 위한 메커니즘입니다. JVM은 이 메커니즘의 여러 구현 간에 전환합니다. 따라서 간단하게 모니터에 대해 이야기할 때 실제로는 잠금에 대해 이야기하고 있습니다. 이 내부 엔터티의 메모리 위치에 대한 참조는 개체 헤더의 마크 워드에 저장됩니다. 따라서 모니터는 실제로 여러 스레드 간에 공유 리소스에 대한 액세스를 동기화하기 위한 메커니즘입니다. JVM은 이 메커니즘의 여러 구현 간에 전환합니다. 따라서 간단하게 모니터에 대해 이야기할 때 실제로는 잠금에 대해 이야기하고 있습니다. 더 나은 조합: Java와 Thread 클래스.  파트 II — 동기화 - 7

동기화됨(잠금 대기 중)

앞에서 보았듯이 "동기화 블록"(또는 "임계 영역")의 개념은 모니터의 개념과 밀접한 관련이 있습니다. 예를 살펴보십시오.

public static void main(String[] args) throws InterruptedException {
	Object lock = new Object();

	Runnable task = () -> {
		synchronized(lock) {
			System.out.println("thread");
		}
	};

	Thread th1 = new Thread(task);
	th1.start();
	synchronized(lock) {
		for (int i = 0; i < 8; i++) {
			Thread.currentThread().sleep(1000);
			System.out.print(" " + i);
		}
		System.out.println(" ...");
	}
}
여기서 메인 쓰레드는 먼저 작업 객체를 새 쓰레드에게 전달한 후 즉시 잠금을 획득하고 이를 가지고 긴 작업을 수행합니다(8초). synchronized이 모든 시간 동안 작업은 이미 잠금을 획득했기 때문에 블록 에 들어갈 수 없기 때문에 작업을 진행할 수 없습니다 . 스레드가 잠금을 얻을 수 없으면 모니터를 기다립니다. 잠금을 확보하는 즉시 실행을 계속합니다. 스레드가 모니터를 종료하면 잠금이 해제됩니다. JVisualVM에서는 다음과 같이 표시됩니다. 더 나은 조합: Java와 Thread 클래스.  2부 — 동기화 - 8JVisualVM에서 볼 수 있듯이 상태는 "모니터"이며 스레드가 차단되어 모니터를 사용할 수 없음을 의미합니다. 코드를 사용하여 스레드의 상태를 결정할 수도 있지만 이 방법으로 결정된 상태의 이름은 JVisualVM에서 사용되는 이름과 비슷하지만 일치하지 않습니다. 이 경우,th1.getState()for 루프의 명령문은 BLOCKED 를 반환합니다 . 루프가 실행되는 동안 객체의 모니터는 스레드 lock에 의해 점유되고 스레드는 차단되어 잠금이 해제될 때까지 진행할 수 없기 때문입니다. 동기화된 블록 외에도 전체 분석법을 동기화할 수 있습니다. 예를 들어 다음은 클래스의 메서드입니다 . mainth1HashTable

public synchronized int size() {
	return count;
}
이 메서드는 주어진 시간에 하나의 스레드에서만 실행됩니다. 자물쇠가 정말 필요한가요? 예, 필요합니다. 인스턴스 메서드의 경우 "이" 개체(현재 개체)가 잠금 역할을 합니다. 여기 이 주제에 대한 흥미로운 논의가 있습니다. 동기화된 블록 대신 동기화된 방법을 사용하면 이점이 있습니까? . 메서드가 정적이면 잠금은 "this" 개체가 아니라(정적 메서드에 대한 "this" 개체가 없기 때문에) Class 개체(예: )가 됩니다 Integer.class.

기다리십시오(모니터 대기 중). notify() 및 notifyAll() 메소드

Thread 클래스에는 모니터와 연결된 또 다른 대기 메서드가 있습니다. sleep()및 와 달리 join()이 메서드는 단순히 호출할 수 없습니다. 그 이름은 wait(). 이 wait메서드는 기다리려는 모니터와 연결된 개체에서 호출됩니다. 예를 보자:

public static void main(String []args) throws InterruptedException {
	    Object lock = new Object();
	    // The task object will wait until it is notified via lock
	    Runnable task = () -> {
	        synchronized(lock) {
	            try {
	                lock.wait();
	            } catch(InterruptedException e) {
	                System.out.println("interrupted");
	            }
	        }
	        // After we are notified, we will wait until we can acquire the lock
	        System.out.println("thread");
	    };
	    Thread taskThread = new Thread(task);
	    taskThread.start();
        // We sleep. Then we acquire the lock, notify, and release the lock
	    Thread.currentThread().sleep(3000);
	    System.out.println("main");
	    synchronized(lock) {
	        lock.notify();
	    }
}
JVisualVM에서는 다음과 같이 표시됩니다. 더 나은 조합: Java와 Thread 클래스.  파트 II - 동기화 - 10이것이 작동하는 방식을 이해하려면 wait()notify()메서드가 와 연결되어 있음 을 기억하십시오 java.lang.Object. 스레드 관련 메서드가 Object클래스에 있는 것이 이상하게 보일 수 있습니다. 그러나 이제 그 이유가 밝혀집니다. Java의 모든 객체에는 헤더가 있다는 것을 기억할 것입니다. 헤더에는 모니터에 대한 정보(예: 잠금 상태)를 포함하여 다양한 관리 정보가 포함되어 있습니다. 각 개체 또는 클래스의 인스턴스는 고유 잠금 또는 모니터라고 하는 JVM의 내부 엔터티와 연결되어 있습니다. 위의 예에서 작업 개체에 대한 코드는 개체와 연결된 모니터에 대해 동기화된 블록을 입력했음을 나타냅니다 lock. 이 모니터에 대한 잠금 획득에 성공하면wait()호출됩니다. 작업을 실행하는 스레드는 lock개체의 모니터를 해제하지만 개체의 모니터에서 알림을 기다리는 스레드 대기열에 들어갑니다 lock. 이 스레드 대기열을 WAIT SET이라고 하며 그 목적을 더 적절하게 반영합니다. 즉, 대기열보다 집합에 가깝습니다. 스레드 main는 작업 개체로 새 스레드를 만들고 시작하고 3초 동안 기다립니다. 이렇게 하면 새 스레드가 스레드보다 먼저 잠금을 획득 main하고 모니터 큐에 들어갈 가능성이 높아집니다. 그런 다음 main스레드 자체가 개체의 동기화 블록에 들어가 lock모니터를 사용하여 스레드 알림을 수행합니다. 알림이 전송된 후 main스레드는locklock객체의 모니터와 이전에 객체의 모니터가 해제되기를 기다리고 있던 새 스레드가 실행을 계속합니다. notify()하나의 스레드( )에만 알림을 보내거나 대기열의 모든 스레드( )에 동시에 알림을 보낼 수 있습니다 notifyAll(). 자세한 내용은 Java에서 notify()와 notifyAll()의 차이점을 참조하십시오 . 알림 순서는 JVM이 구현되는 방식에 따라 다르다는 점에 유의해야 합니다. 자세한 내용은 여기를 참조하십시오. notify 및 notifyAll을 사용하여 기아 문제를 해결하는 방법은 무엇입니까? . 개체를 지정하지 않고 동기화를 수행할 수 있습니다. 단일 코드 블록이 아닌 전체 메서드가 동기화될 때 이 작업을 수행할 수 있습니다. .class예를 들어, 정적 메서드의 경우 잠금은 Class 개체(를 통해 얻음 ) 가 됩니다 .

public static synchronized void printA() {
	System.out.println("A");
}
public static void printB() {
	synchronized(HelloWorld.class) {
		System.out.println("B");
	}
}
잠금 사용 측면에서 두 방법은 동일합니다. instance메서드가 정적이 아닌 경우 현재 , 즉 를 사용 하여 동기화가 수행됩니다 this. 그건 그렇고, 우리는 이전에 이 getState()메서드를 사용하여 스레드의 상태를 얻을 수 있다고 말했습니다. 예를 들어 모니터를 기다리는 대기열의 스레드의 경우 메서드가 wait()시간 초과를 지정한 경우 상태는 WAITING 또는 TIMED_WAITING이 됩니다. 더 나은 조합: Java와 Thread 클래스.  파트 II — 동기화 - 11

https://stackoverflow.com/questions/36425942/what-is-the-lifecycle-of-thread-in-java

스레드 수명 주기

수명이 다하는 동안 스레드의 상태가 변경됩니다. 실제로 이러한 변경 사항은 스레드의 수명 주기를 구성합니다. 스레드가 생성되자마자 상태는 NEW입니다. 이 상태에서 새 스레드는 아직 실행 중이 아니며 Java 스레드 스케줄러는 아직 이에 대해 아무것도 알지 못합니다. 스레드 스케줄러가 스레드에 대해 학습하려면 thread.start()메서드를 호출해야 합니다. 그런 다음 스레드는 RUNNABLE 상태로 전환됩니다. 인터넷에는 "실행 가능" 상태와 "실행 중" 상태를 구분하는 잘못된 다이어그램이 많이 있습니다. 그러나 이것은 실수입니다. Java는 "작업 준비"(실행 가능)와 "작업"(실행 중)을 구분하지 않기 때문입니다. 스레드가 활성 상태이지만 활성화되지 않은 경우(실행 가능하지 않은 경우) 다음 두 가지 상태 중 하나입니다.
  • BLOCKED — 크리티컬 섹션, 즉 synchronized블록에 진입하기 위해 대기 중입니다.
  • WAITING — 다른 스레드가 어떤 조건을 만족시키기를 기다립니다.
조건이 만족되면 스레드 스케줄러가 스레드를 시작합니다. 스레드가 지정된 시간까지 대기 중인 경우 해당 상태는 TIMED_WAITING입니다. 스레드가 더 이상 실행 중이 아니면(완료되었거나 예외가 발생한 경우) TERMINATED 상태가 됩니다. 스레드의 상태를 찾으려면 getState()메서드를 사용하십시오. isAlive()스레드에는 스레드가 종료되지 않은 경우 true를 반환하는 메서드 도 있습니다 .

LockSupport 및 스레드 파킹

Java 1.6부터 LockSupport 라는 흥미로운 메커니즘이 등장했습니다. 더 나은 조합: Java와 Thread 클래스.  파트 II — 동기화 - 12이 클래스는 "허가"를 사용하는 각 스레드와 연결합니다. 메소드 에 대한 호출은 park()허가가 사용 가능한 경우 즉시 반환되어 프로세스에서 허가를 소비합니다. 그렇지 않으면 차단됩니다. 메서드를 호출하면 unpark아직 사용할 수 없는 경우 허가를 사용할 수 있습니다. 허가증은 1개뿐이다. 에 대한 Java 문서는 클래스를 LockSupport참조합니다 Semaphore. 간단한 예를 살펴보겠습니다.

import java.util.concurrent.Semaphore;
public class HelloWorldApp{
    
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(0);
        try {
            semaphore.acquire();
        } catch (InterruptedException e) {
            // Request the permit and wait until we get it
            e.printStackTrace();
        }
        System.out.println("Hello, World!");
    }
}
이제 세마포어에 0개의 허가가 있기 때문에 이 코드는 항상 대기합니다. 그리고 acquire()코드에서 가 호출되면(예: 허가 요청) 스레드는 허가를 받을 때까지 기다립니다. 우리는 기다리고 있기 때문에 처리해야 합니다 InterruptedException. 흥미롭게도 세마포어는 별도의 스레드 상태를 갖습니다. JVisualVM을 보면 상태가 "Wait"이 아니라 "Park"임을 알 수 있습니다. 더 나은 조합: Java와 Thread 클래스.  2부 — 동기화 - 13다른 예를 살펴보겠습니다.

public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            // Park the current thread
            System.err.println("Will be Parked");
            LockSupport.park();
            // As soon as we are unparked, we will start to act
            System.err.println("Unparked");
        };
        Thread th = new Thread(task);
        th.start();
        Thread.currentThread().sleep(2000);
        System.err.println("Thread state: " + th.getState());
        
        LockSupport.unpark(th);
        Thread.currentThread().sleep(2000);
}
스레드의 상태는 WAITING이지만 JVisualVM은 wait키워드 synchronizedpark클래스 를 구별합니다 LockSupport. 이것이 왜 LockSupport그렇게 중요한가요? Java 문서로 다시 돌아가서 WAITING 스레드 상태를 살펴봅니다. 보시다시피, 들어가는 방법은 세 가지뿐입니다. 그 두 가지 방법은 wait()및 입니다 join(). 그리고 세 번째는 LockSupport. Java에서 잠금은 t에 구축될 수 LockSuppor있으며 더 높은 수준의 도구를 제공합니다. 한 번 사용해 봅시다. 예를 들어 다음을 살펴보십시오 ReentrantLock.

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HelloWorld{

    public static void main(String []args) throws InterruptedException {
        Lock lock = new ReentrantLock();
        Runnable task = () -> {
            lock.lock();
            System.out.println("Thread");
            lock.unlock();
        };
        lock.lock();

        Thread th = new Thread(task);
        th.start();
        System.out.println("main");
        Thread.currentThread().sleep(2000);
        lock.unlock();
    }
}
앞의 예에서와 마찬가지로 여기서도 모든 것이 간단합니다. 개체 lock는 누군가가 공유 리소스를 해제하기를 기다립니다. JVisualVM을 보면 스레드가 main잠금을 해제할 때까지 새 스레드가 파킹되는 것을 볼 수 있습니다. 잠금에 대한 자세한 내용은 Java 8 StampedLocks 대 ReadWriteLocks and Synchronized and Lock API in Java에서 확인할 수 있습니다. 잠금이 구현되는 방식을 더 잘 이해하려면 Java Phaser 가이드 문서에서 Phaser에 대해 읽어보는 것이 좋습니다 . 그리고 다양한 동기화 프로그램에 대해 말하자면 The Java Synchronizers에 대한 DZone 기사를 읽어야 합니다 .

결론

이 리뷰에서는 스레드가 Java에서 상호 작용하는 주요 방식을 조사했습니다. 추가 자료: 더 나은 조합: Java와 Thread 클래스. 1부 — 실행 스레드 Java와 Thread 클래스를 함께 사용하면 더 좋습니다. 3부 — 상호 작용 더 나은 조합: 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