JAVA/JAVA Basic

[ JAVA ] 스레드(Thread) 동기화4 ( yield(), join() )

IT록흐 2021. 6. 20. 09:20
반응형

 

 

 

[ JAVA ] 스레드(Thread) 동기화1 ( Intrinsic Lock + 피터슨 알고리즘 )

동기화 지훈이네 가족은 아빠, 엄마 그리고 형까지 네 식구로 이루어져 있다. 그러나 집 안에 컴퓨터가 단 한 대 뿐이라 가족간의 충돌이 불가피했다. 그래서 지훈이네 가족은 컴퓨터를 쓰는 순

lordofkangs.tistory.com

 

 

위 포스팅에서

 

동기화는

'상호배제(Mutex)'와 '협동(Cooperation)'으로

이루어진다고 말했다.

 

 

이전 포스팅 '스레드 동기화 이해하기 1,2,3'에서는 상호배제(Mutex) 방법으로 피터슨 알고리즘, 세마포어, 모니터를 알아보았다. 이번 포스팅에서는 동기화 작업을 위한 협동(Cooperation)에 대해서 알아볼 것이다.

 

동기화는 작업 순서를 정하는 것이다. 그 안에서 '협동'의 의미란, 작업을 끝낸 스레드가 다른 스레드에게 "나 작업 끝났어!"라고 알려주는 것이다. 이렇게 스레드 간의 소통을 만들어, 동기화 작업을 원할하게 만드는 것이 협동(Cooperation)이다.

 

그럼 협동 하기전, 스레드가 가질 수 있는 상태가 무엇인지 알아보자.

 

스레드 상태

 

 

JAVA API 문서

 

 

JAVA API 문서에서 Thread에 대한 내용을 보면, Thread는 중첩 static 클래스로 State 클래스를 갖고 있음을 알 수 있다. 중첩클래스란 한 클래스 블록 안에서 정의된 또 하나의 클래스를 의미한다. 재미있는 것 중첩된 State 클래스는 보통의 클래스가 아니다.

 

JAVA API 문서

 

 

중첩된 State 클래스는 열거 타입의 클래스이다. 열거타입은 한 가지 대상이 상황에 따라 여러 상태로 바뀔 때, 주로 사용하는 클래스이다. 예를 들면, 지훈이는 학교에서는 모범생이라 불리고, 친구들 사이에서는 별명으로 불리고 집에서는 아들이라 불리는 것과 같다. 한 가지 대상의 호칭이 상황에 따라 여러가지로 바뀌는 것이다.

 

 

[ JAVA ] 열거타입(enumeration type)

열거 타입은 이름마저 생소하다. 열거타입을 사용하는 이유는 무엇일까? 간단히 예를 들어보겠다. '김철수'라는 사람이 있다. 철수는 회사에서 '김대리'라 불리지만 집에서는 '아빠'라 불린다.

lordofkangs.tistory.com

 

자세한 내용은 해당 포스팅에 정리해두었으니 참고하시면 됩니다.

 

이는 스레드도 마찬가지다. 스레드는 상황에 따라 여러가지 상태로 바뀐다. 스레드의 상태는 4가지 상태가 있다.

 

1. 객체 생성 : NEW

2. 실행 대기 : RUNNABLE

3. 일시정지 : WAITING, TIMED_WAITING, BLOCKED

4. 종료 : TERMINATED

 

4가지 상태는 다음과 같은 열거 상수를 갖는다. 열거상수를 한번 사용해보자.

 

< 스레드 상태 체크하는 Thread >

package threadstate;

public class CheckThreadState extends Thread {

	Target target;
	
	public CheckThreadState(Target target) {
		this.target = target;
	}
	
	@Override
	public void run() {
		
		while(true) {
			Thread.State state = target.getState(); // 열거형 객체 생성 
			System.out.println("현재 스레드 상태 : "+ state); // 열거 상수 출력
			
			if(state == Thread.State.NEW) { 
				target.start(); 
			}
			
			if(state == Thread.State.TERMINATED) {
				System.out.println("스레드 종료");
				break;
			}
			
			try {
				Thread.sleep(100); 
			} catch (Exception e) {
				// TODO: handle exception
			}
		}
	}
}

 

< 타겟 스레드 >

package threadstate;

public class Target extends Thread {
	
	@Override
	public void run() {
		
		
		for (long i = 0; i < 1000000000; i++) {} // RUNNABLE
	
		try {
			Thread.sleep(500);                   // TIMED_WAITING
		} catch (Exception e) {
			// TODO: handle exception
		}
		
		for (long i = 0; i < 1000000000; i++) {} // RUNNABLE	
	}

}

 

< Main 클래스 >

package threadstate;

public class Main {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		CheckThreadState cts = new CheckThreadState(new Target());
		cts.start();
	}
}

 

<출력 결과>

 

이처럼 스레드는 NEW -> RUNNABLE -> TIMED_WAITING -> RUNNABLE -> TERMINATED 상태로 변화되어감을 알 수 있다. 이와 같이 스레드는 다양한 상태로 존재한다. 이런 상태는 스레드간의 작업순서를 정할 때 중요한 역할을 한다. 작업순서를 정하는 구체적인 방법으로 먼저 Thread 클래스의 yield()와 join()을 살펴보겠다.

 

yield()

 

 

JAVA API 문서

 

yield()는 실행 중인 스레드를 RUNNABLE (실행 대기) 상태로 바꾸는 메소드이다.

yield()의 사용을 예를 들어 설명하면, 추운 겨울, 지훈이는 민정이와 2시와 3시 사이에 백화점 앞에서 만나기로 약속을 잡았다. 2시에 백화점 앞에 도착한 지훈이는 민정이가 정확히 2시 몇분에 도착할지 모른다. 그래서 지훈이는 추운 겨울 밖에서 기다리기 보다는 잠시 백화점 안의 카페에 들어가 몸을 녹이며 기다리기로 결정한다.

 

이와 같이, 스레드가 실행되었는데 스레드의 실행이 잠시동안 무의미한 경우가 존재한다. 이런 경우 스레드를 잠시 실행 대기 상태로 돌려놓으면 좋다. 그래야 CPU의 자원의 소모를 방지할 수 있다.

 

package threadyield;

public class Car extends Thread {
	
	TrafficLights tl; // 신호등 스레드 
	
	public Car(TrafficLights tl) {
		this.tl = tl;
	}
	
	@Override
	public void run() {
		
		while(true) {
			
			//신호등이 초록불인 경우
			if(tl.getSignal()) { 
				System.out.println("초록불이다! 신나게 달리자!");
			}

			//신호등이 빨간불인 경우
			else {
				System.out.println("빨간불이다! 잠시 정지!");
				Thread.yield(); // 자동차 스레드 => 실행 대기
			}
			try {
				Thread.sleep(500);
			} catch (Exception e) {
				// TODO: handle exception
			}
		}
	}
}

 

 

위 코드와 같이, 신호등 스레드가 초록불을 가리키면, 열심히 스레드를 실행시키다가 불특정 시점에 신호등 스레드가 빨간불을 가리키면, 스레드는 Thread.yield()를 통해 자신이 점유하고 있던 CPU자원을 내려놓고 실행대기(RUNNABLE) 상태로 들어간다.

 

 

<출력 결과>

 

신호등이 초록불이든 빨간불이든 스레드의 현재 상태는 RUNNABLE(실행대기)임을 알 수 있다. CPU 할당을 받았느냐 받지 못했느냐에 따라, 실행이냐 실행대기이냐 구분을 하는데, Thread.State에서는 실행과 실행대기를 둘다 RUNNABLE로 표현한다.

 

그러나 나는 출력결과를 보고 당황했다. 신호등이 빨간불이면 반복을 잠시 멈추고 실행대기상태로 가서, "빨간불이다! 잠시 정지!"라는 멘트가 반복하여 출력되지 않을 줄 알았다. 하지만 CPU를 내려놓았는데도 불구하고 출력이 계속되었다.

 

정확히 이유는 알지 못하였다. 추측을 조금 해보자면, 실행 대기상태(RUNNABLE)는 언제든 실행이 가능한 상태이다. 그러므로 CPU에 대한 스레드 경쟁이 없는 상태에서 양보를 해도, 양보를 받을 스레드가 없으니 양보를 하고 대기상태로 돌아간 스레드가 다시 실행된 것 같다.

 

join()

 

 

JAVA API 문서

 

join() 다른 스레드가 종료될 때까지 일시정지 됐다가 종료가 되면 다시 실행되도록 명령하는 메소드다.

 

간단히 말해서 A 스레드가 실행(RUNNABLE)되다가 B.join()을 만나면, B 스레드가 종료될 때까지, A스레드는 그 자리에서 일시정지(WAITING) 된다. 그리고 B 스레드가 종료되면 A 스레드는 다시 실행된다(RUNNABLE).

 

 

<Main 클래스>

public static void main(String[] args) {
		// TODO Auto-generated method stub
        
       //스레드 생성
		Thread a = new Thread(new Run());
        Thread b = new Thread(new Run());
        
       // 스레드 실행
        a.start();
        b.start();
        
       // Main 스레드 일시정지
        a.join();
        b.join();

        System.out.println("Main 끝!");
	}

 

 

a.join() 만나면 Main 스레드는 실행을 정지하고 WAITING 상태가 된다. 그 후 a 스레드가 끝나면 b 스레드가 끝날 때까지 기다린다. 마지막으로 b 스레드가 끝나면 Main 스레드가 다시 실행된다.

 

 


정리

 

 

1. 스레드의 상태는 열거상수로 정의된다.

2. 동기화는 상호배제와 스레드 간의 소통(Cooperation)을 통해 이루어진다.

3. yield()는 cpu자원을 다른 스레드에게 양보한다.

4. join()은 다른 스레드가 종료될 때까지 일시정지된다.

 

 


참고자료

 

 

 

반응형