멀티 스레드 02

오늘·2021년 3월 30일
0

Java

목록 보기
34/42

스레드 상태

스레드 객체를 생성하고, start() 메소드를 호출하면 곧바로 스레드가 실행되는 것처럼 보이지만 사실은 실행 대기 상태가 된다

(실행대기 상태란, 아직 스케줄링이 되지 않아 실행을 기다리고 있는 상태)

실행 대기 상태에 있는 스레드 중 스케줄링으로 선택된 스레드가 비로서 CPU를 점유하고 run()메소드를 실행할 수 있고, 이를 실행(Running) 상태라 한다.


스레드는 실행 대기 상태와 실행 상태를 번갈아가면서 자신의 run() 메소드를 조금씩 실행한다. 실행 상태에서 run() 메소드가 종료되면, 더 이상 실행할 코드가 없기때문에 스레드의 실행은 멈추게 되고 이 상태를 종료 상태라 한다.

경우에 따라 실행 상태에서 실행 대기 상태로 가는 것이 아니라, 일시 정지 상태로 가기도한다.

이런 스레드의 상태를 코드에서 확인할 수 있도록 하기 위해 Thread 클래스에 getState() 메소드가 추가 되었다.

스레드의 상태를 출력하기

타겟 스레드의 상태를 0.5 초 주기로 출력한다

// 타깃 스레드의 상태를 출력하는 스레드
public class StatePrintThread extends Thread {
	private Thread targetThread;
	
	//Thread targetThread = 상태를 조사할 메소드
	public StatePrintThread(Thread targetThread) {
		this.targetThread = targetThread;
	}
	
	// 스레드에 들어갈 내용
	// 스레드에서 처리할 내용
	public void run() {
		while(true) {
			// 스레드 상태 얻기
			Thread.State state = targetThread.getState();
			System.out.println("타켓 스레드 상태 : " + state);
			
			// 객체 생성 샅애일 경우 실행 대기 상태로 만든다
			if (state == Thread.State.NEW) {
				targetThread.start();
			}
			
			// 종료 상태일 경우 while문을 종료
			if(state == Thread.State.TERMINATED) {
				break;
			}
			try {
				// 0.5초간 일시 정지
				Thread.sleep(500);
			}catch (Exception e) { }
		}
	}
}



// 타겟 스레드
// 객체 생성시 NEW 상태를 자기고
// run()메소드가 종료되면 TERMINATED상태가 된다
public class TargetThread extends Thread {
	public void run() {
		for(long i=0; i<1000000000; i++) {	}
		
		try {
			// 1.5초간 일시 정지
			Thread.sleep(1500);
		}catch (Exception e) {	}
		
		for(long i=0; i<000000000; i++) {	}
	}
}


class ThreadStateEx {
	public static void main(String[] args) {
		StatePrintThread spt = new StatePrintThread(new TargetThread());
		spt.start();
	}
}

스레드 상태 제어

상태 제어
: 실행중인 스레드의 상태를 변경하는 것
: 상태 변화를 가져오는 메소드의 종류

상태제어가 잘못되면 프로그램은 불안정해져서 먹통이 되거나 다운된다. 스레드는 잘 사용하면 약이 되지만, 잘못 사용하면 치명적인 프로그램 버그가 되기 때문에 스레드를 정확하게 제어하는 방법을 잘 알고 있어야 한다.

다음 표는 상태 변화를 가져오는 메소드의 종류를 보여준다.


sleep() : 주어진 시간동안 일시 정지

Thread.sleep() 메소드를 호출한 스레드는 주어진 시간 동안 일시 정지 상태가 되고, 다시 실행 대기 상태로 돌아간다

try {
	// 1초동안 일시정지
	Thread.sleep(1000);
} catch(InterruptedException e) {
	// interrupt() 메소드가 호출되면 실행
}

매개 값에는 얼마동안 일시 정지 상태로 있을 것인지 (1/1000) 단위로 시간을 주면 된다. 일시정지 상태에서 주어진 시간이 되기 전에 interrupt() 메소드가 호출되면 exception이 발생하기 때문에 예외 처리가 필요하다.

sleep()을 사용한 예제

3초 주기로 10번 비프음이 발생한다.

import java.awt.Toolkit;

public class Sleep {
	public static void main(String[] args) {
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		for(int i=0; i<10; i++) {
			toolkit.beep();
			try {
            			// 3초동안 일시정지 상태로 보내고
                        	// 3초가 지나면 다시 실행 준비 상태로 돌아돈다
				Thread.sleep(3000);
			} catch (Exception e) {	}
		}
	}
}

yield() : 다른 스레드에게 실행 양보

스레드가 처리하는 작업은 반복적인 실행을 위해 for문이나 while문을 포함하는 경우가 많다. 가끔은 이 반복문들이 무의미한 반복을 하는 경우가 있는데, 그것보다는 다른 스레드에게 실행을 양보하고 자신은 실행 대기 상태로 가는 것이 전체 프로그램 성능에 도움이 된다.

yield() 메소드를 호출한 스레드는 실행 대기 상태로 돌아가고, 동일한 또는 높은 우선순위를 가지는 다른 스레드가 실행 기회를 가질 수 있도록 해준다.

public void run() {
	while(true) {
    		if(work) {
            		System.out.println("ThreadA의 작업내용");
                } else {
                	Thread.yield();
                }
        }
}

위 코드와 같이 의미없는 반복을 줄이기 위해 yield() 메소드를 호출해 다른 스레드에게 실행 기회를 주도록 한 것이다.

yield()를 사용한 예제

// 스레드 실행 양보 예제
public class YieldEx {
	public static void main(String[] args) {
		ThreadA ta = new ThreadA();
		ThreadB tb = new ThreadB();
		
		// A와 B 모두 실행
		ta.start();
		tb.start();
		
        
		try {
        		// 3초가 지나면
			Thread.sleep(3000);
		}catch (InterruptedException e) {	}
        	// A의 work 필드를 false로 변경해
            	//B만 실행된다.
		ta.work = false;
		
        
		try {
        		// 3초가 지나면
			Thread.sleep(3000);
		}catch (InterruptedException e) {	}
        	// A의 work필드를 다시 true로 변경하고
               	// A와 B는 모두 실행된다
		ta.work =true;
		

		try {
        		// 3초가 지나면
			Thread.sleep(3000);
		}catch (InterruptedException e) {	}
        	// A만 실행된다
		tb.work = false;
		

		try {
        		// 3초가 지나면
			Thread.sleep(3000);
		}catch (InterruptedException e) {	}
       		// A와 B는 모두 종료된다
		ta.stop = true;
		tb.stop = true;
	}
}

//---------------------------------------------------------------------
class ThreadA extends Thread {
	public boolean stop = false;
	public boolean work = true;
	
	@Override
	public void run() {
		while(!stop) {
			// 실행속도가 너무 빨라 확인이 어려워서
			// 임의로 sleep을 지정해줌
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {	}
			
			if(work) {
				System.out.println("ThreadA의 작업 내용");
			} else {
				System.out.println("A가 양보--------------");
				Thread.yield();
			}
		}
		System.out.println("ThreadA의 작업 종료");
	}
}

//---------------------------------------------------------------------
class ThreadB extends Thread {
	public boolean stop = false;
	public boolean work = true;
	
	@Override
	public void run() {
		while(!stop) {
			// 실행속도가 너무 빨라 확인이 어려워서
			// 임의로 sleep을 지정해줌
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {	}
			
			if(work) {
				System.out.println("ThreadB의 작업 내용");
			} else {
				System.out.println("B가 양보-----------------");
				Thread.yield();
			}
		}
		System.out.println("ThreadB 종료");
	}
}

양보시키려는 스레드에 yield()를 넣어주는 것이다.


join() : 다른 스레드의 종료를 기다림

스레드는 다른 스레드와 독립적으로 실행하는 것이 기본이지만, 다른 스레드가 종료될 때까지 기다렸다가 실행해야 하는 경우도 있다. 예를 들어 모든 계산 작업을 마쳤을 때 계산 결과값을 받아 이용하는 경우가 이에 해당한다.

join() 을 사용한 예제

public class JoinEx {
	public static void main(String[] args) {
		SumThread st = new SumThread();
		st.start();

		// 이 try-catch문을 주석처리하면
		// SumThread가 일을 마치기도 전에 메인 스레드가 출력되면서
		// 원하는 결과가 나오지 않는다.
		try {
			// SumThread가 종료할 때까지 메인 스레드를 일시 정지시킨다
			st.join();
		}catch (InterruptedException e) {		}

		System.out.println("1~100까지의 합 : " + st.getSum());
	}
}


class SumThread extends Thread {
	private long sum;
	
	public long getSum() {
		return sum;
	}
	
	public void setSum(long sum) {
		this.sum = sum;
	}
	
	public void run() {
		for(int i=1; i<=100; i++) {
			sum += i;
		}
		System.out.println("SumThread 부분입니다.");
	}
}

실행 결과
1. join()을 사용하지 않았을 때

1~100까지의 합 : 0
SumThread 부분입니다.

계산이 끝나면 값을 받아와야 하는데, 메인 스레드가 먼저 실행되면서 결과값이 제대로 출력되지 않는 모습

2. join()을 사용하였을 때

SumThread 부분입니다.
1~100까지의 합 : 5050

계산하는 sumThread가 먼저 실행이 끝날 때까지 기다렸다가 메인이 실행되기 때문에 원하는 모양으로 결과값이 출력되는 것을 확인할 수 있었다.


요구사항
: 작업스레드 1 = 1~3까지의 합 구하기
: 작업스레드 2 = 4~6까지의 합 구하기
: 작업스레드 3 = 7~10까지의 합 구하기

: 메인스레드에서 작업스레드 1,2,3의 결과를 합쳐서 출력

join() 사용해보기 01

  1. 계산하는 스레드를 각각 만들어주었다.
  2. 각 스레드가 끝나기를 기다렸다가 결과합을 메인에서 구해주었다.

전체 코드

public class JoinEx_Test {
	public static void main(String[] args) {
		A a = new A();
		B b = new B();
		C c = new C();
		
		a.start();
		b.start();
		c.start();
		
		try { a.join();
		}catch (InterruptedException e) {	}
		try { b.join();
		}catch (InterruptedException e) {	}
		try { c.join();
		}catch (InterruptedException e) {	}
		
		int hap = a.getA() + b.getB() + c.getC();
		System.out.println("작업스레드의 결과 합 : " + hap);
	}
}

class A extends Thread {
	int A;
	public int getA() {
		return A;
	}
	public void setA(int A) {
		this.A = A;
	}
	public void run() {
		for(int i=1; i<=3; i++) {
			A += i;
		}
		System.out.println("1~3을 더히는 부분 : " + A);
	}
}

class B extends Thread {
	int B;
	public int getB() {
		return B;
	}
	public void setB(int B) {
		this.B = B;
	}
	public void run() {
		for(int i=4; i<=6; i++) {
			B += i;
		}
		System.out.println("4~6을 더히는 부분 : " + B);
	}
}

class C extends Thread {
	int C;
	public int getC() {
		return C;
	}
	public void setB(int C) {
		this.C = C;
	}
	public void run() {
		for(int i=7; i<=10; i++) {
			C += i;
		}
		System.out.println("7~10을 더히는 부분 : " + C);
	}
}

실행 모습

1~3을 더히는 부분 : 6
4~6을 더히는 부분 : 15
7~10을 더히는 부분 : 34
작업스레드의 결과 합 : 55

A, B, C의 실행 순서는 뒤바뀔 수 있으나
결과값을 합산해 출력하는 메인이 제일 마지막에 실행되며
원하는 모양대로 출력되는 모습을 확인할 수 있었다.

join() 사용해보기 02

  1. 실행 스레드를 하나만 만들고, 객체 생성하며 매개변수로 값을 달리 주었다.
  2. 각 스레드에서 계산 결과 값을 set-()메소드로 넘겨서 누적해주고
    get-()메소드로 누적된 값을 호출해주었다.
  3. try-catch 모양
try {
	a.join();
	a1.join();
	a2.join();
} catch (InterruptedException e) {
}
// 위나 아래나 동작하는 모습은 똑같다
// try { a.join();
// }catch (InterruptedException e) { }
// try { a1.join();
// }catch (InterruptedException e) { }
// try { a2.join();
// }catch (InterruptedException e) { }

전체 코드

public class JoinEx_T_ {
	public static void main(String[] args) {
		// 스레드 이름, 시작 숫자, 끝 숫자
		A1 a = new A1(1, 1, 3);
		A1 a1 = new A1(2, 4, 6);
		A1 a2 = new A1(3, 7, 10);

		a.start();
		a1.start();
		a2.start();

		try {
			a.join();
			a1.join();
			a2.join();
		} catch (InterruptedException e) {
		}
		// 위나 아래나 동작하는 모습은 똑같다
		// try { a.join();
		// }catch (InterruptedException e) { }
		// try { a1.join();
		// }catch (InterruptedException e) { }
		// try { a2.join();
		// }catch (InterruptedException e) { }


		System.out.println("/");
		System.out.println("더하기 작업이 완료 되었습니다");
		System.out.println("모두 더한 수는 " + a2.getAll_add_num()+ " 입니다");
	}
}

class A1 extends Thread {
	int name, start, end;
	int aHap = 0;
	static int all_add_num = 0; // 전체 스레드의 덧셈값을 누적할 변수

	public A1(int name, int start, int end) {
		this.name = name;
		this.start = start;
		this.end = end;
	}

	public void run() {
		for (int i = start; i <= end; i++) {
			System.out.println("현재 연산하고 있는 스레드 이름은 " + name + " | 계산중인 i 값 : " + i);
			aHap += i;

			// 계산 모습을 편하게 보기 위해 sleep 넣어주기
			try {
				sleep(1000);
			} catch (InterruptedException e) {
			}
		}
		System.out.println("스레드의 총 합계 : " + aHap);
		setAll_add_num(aHap); 
	}

	// 각 스레드의 합을 모두 누적해 주는 메소드
	void setAll_add_num(int num) {
		all_add_num += num;
	}

	// 마지막에 전체의 합이 들어 있는 메소드를 리턴해줌
	int getAll_add_num() {
		return all_add_num;
	}
}

실행 모습

현재 연산하고 있는 스레드 이름은 1 | 계산중인 i 값 : 1
현재 연산하고 있는 스레드 이름은 3 | 계산중인 i 값 : 7
현재 연산하고 있는 스레드 이름은 2 | 계산중인 i 값 : 4
현재 연산하고 있는 스레드 이름은 1 | 계산중인 i 값 : 2
현재 연산하고 있는 스레드 이름은 2 | 계산중인 i 값 : 5
현재 연산하고 있는 스레드 이름은 3 | 계산중인 i 값 : 8
현재 연산하고 있는 스레드 이름은 1 | 계산중인 i 값 : 3
현재 연산하고 있는 스레드 이름은 2 | 계산중인 i 값 : 6
현재 연산하고 있는 스레드 이름은 3 | 계산중인 i 값 : 9
스레드의 총 합계 : 15
현재 연산하고 있는 스레드 이름은 3 | 계산중인 i 값 : 10
스레드의 총 합계 : 6
스레드의 총 합계 : 34
/
더하기 작업이 완료 되었습니다
모두 더한 수는 55 입니다

쭉 계산되는 모습을 하나하나 출력해주었다.
스레드가 순차적이지는 않지만 계산 스레드들 먼저 실행해준 뒤 모두 끝나면 메인이 실행되는 모습을 확인 할 수 있었다.


스레드 간 협업(wait(), notify(), notifyAll())

경우에 따라서는 두 개의 스레드를 교대로 번갈아가며 실행해야 할 경우가 있다. 정확한 교대 작업이 필요한 경우, 그러니까 자신의 작업이 끝나면 상대방의 스레드를 일시 정지 상태에서 풀어주고 자신은 일시 정지 상태로 들어가야한다.

주의할 점은 이 메소드들은 동기화 메소드 또는 동기화 블록 내에서만 사용할 수 있다는 것이다.

A와 B가 교대로 출력

두 스레드의 작업을 WorkObject의 methodA와 methodB에 정의해두고, 두 스레드 ThreadA와 ThreadB가 교대로 메소드를 호출하도록 했다.

// 두 스레드의 작업 내용을 동기화 메소드로 작성한 공유 객체
public class WorkObject {
	// 동기화(sychronized)처리
	public synchronized void methodA() {
		System.out.println("ThreadA의 methodA() 작업 실행");
		// 일시 정지 상태에 있는 ThreadB를 실행 대기 상태로 만든다
		notify();
		// ThreadA를 일시 정지 상태로 만듦
		try {
			wait();
		} catch (InterruptedException e) {
		}
	}
	
	// 동기화(sychronized)처리
	public synchronized void methodB() {
		System.out.println("ThreadB의 methodB() 작업 실행");
		// 일시 정지 상태에 있는 ThreadA를 실행 대기 상태로 만든다
		notify();
		//ThreadB를 일시 정지 상태로 만듦
		try {
			wait();
		}catch (InterruptedException e) {	}
	}
}

--------------------------------------------------------------------------
// workObject의 methodA()를 실행하는 스레드 두 개
public class ThreadA extends Thread{
	private WorkObject workObj;
	
	// 공유 객체를 매개값으로 받아 필드에 저장
	public ThreadA(WorkObject workObj) {
		this.workObj = workObj;
	}
	
	// 공유 객체의 methodA()를 10번 반복 호출
	@Override
	public void run() {
		for(int i=0; i<10; i++) {
			workObj.methodA();
		}
	}
}

class ThreadB extends Thread {
	private WorkObject workObj;
	
	// 공유 객체에 매개값으로 받아 필드에 저장
	public ThreadB(WorkObject workObj) {
		this.workObj = workObj;
	}
	
	// 공유 객체의 methodB()를 10번 반복 호출
	@Override
	public void run() {
		for(int i=0; i<10; i++) {
			workObj.methodB();
		}
	}
}

--------------------------------------------------------------------------

// 두 스레드를 생성하고 실행하는 메인 스레드
public class WaitNotifyEx {
	public static void main(String[] args) {
		// 공유 객체 생성
		WorkObject sharedObj = new WorkObject();
		
		ThreadA threadA = new ThreadA(sharedObj);
		ThreadB threadB = new ThreadB(sharedObj);
		
		threadA.start();
		threadB.start();
	}
}

실행 모습

…
ThreadA의 methodA() 작업 실행
ThreadB의 methodB() 작업 실행
ThreadA의 methodA() 작업 실행
ThreadB의 methodB() 작업 실행
ThreadA의 methodA() 작업 실행
ThreadB의 methodB() 작업 실행
ThreadA의 methodA() 작업 실행
ThreadB의 methodB() 작업 실행
ThreadA의 methodA() 작업 실행
ThreadB의 methodB() 작업 실행
ThreadA의 methodA() 작업 실행
ThreadB의 methodB() 작업 실행
…

10번씩 번갈아가며 실행되는 모습을 확인할 수 있었다.


번갈아가며 실행 (생산자-소비자)

생산자-소비자 코드

:생산자 스레드는 소비자 스레드가 읽기 전에 새로운 데이터를 두 번 생성하면 안되고
(= setData() 메소드를 두 번 실행하면 안 된다)

: 소비자 스레드는 생산자 스레다가 새로운 데이터를 생성하기 전에 이전 데이터를 두 번 읽어서도 안된다
(=getData() 메소드를 두 번 실행하면 안됨)

: data필드 값이 null 이라면
생산자 스레드를 실행 대기 상태로 만들고, 소비자 스레드를 일시 정지 상태로 만들어 놓는다

: data 필드의 값이 null이 아니라면
소비자 스레드를 실행 대기 상태로 만들고, 생산자 스레드를 일시 정지 상태로 만든다.

-> 데이터를 저장하는 생산자 스레드가 데이터를 저장하면
데이터를 소비하는 소비자 스레드가 데이터를 읽고 처리하는 교대 작업

// 두 스레드의 작업 내용을 동기화 메소드로 작성한 공유 객체
public class DataBox {
	private String data;

	public synchronized String getData() {
		// data 필드가 null이면 소비자 스레드를 일시 정지 상태로 만든다
		if (this.data == null) {
			try {
				wait();
			} catch (InterruptedException e) {	}
		}
		String returnValue = data;
		System.out.println("ConsummerThread가 읽은 데이터 : " + returnValue);
		// data 필드를 null로 만들고 생산자 스레드를 실행 대기 상태로 만듦
		data = null;
		notify();
		return returnValue;
	}
	
	public synchronized void setData(String data) {
		// data 필드가 null이 아니라면 생산자 스레드를 일시정지 상태로 만듦
		if(this.data != null) {
			try {
				wait();
			}catch (InterruptedException e) {	}
		}
		// data필드에 값을 저장하고 소비자 스레드를 실행 대기 상태로 만든다
		this.data = data;
		System.out.println("ProducerThread가 생성한 데어터 : " + data);
		notify();
	}
}


--------------------------------------------------------------------------
// 생산자-소비자 스레드
public class Producer_Consumer {}

class ProducerThread extends Thread {
	private DataBox databox;
	
	// 공유 객체를 필드에 저장
	public ProducerThread(DataBox dataBox) {
		this.databox = dataBox;
	}
	// 새로운 데이터를 넣어주기
	@Override
	public void run() {
		for(int i=1; i<=3; i++) {
			String data = "Data-" + i;
			databox.setData(data);
		}
	}
}

class ConsumerThread extends Thread {
	private DataBox databox;
	
	// 공유 객체르 ㄹ필드에 저장
	public ConsumerThread(DataBox databox) {
		this.databox = databox;
	}
	// 새로운 데이터를 읽어오기
	@Override
	public void run() {
		for(int i=1; i<=3; i++) {
			String data = databox.getData();
		}
	}
}

-----------------------------------------------------------------------
// 두 스레드를 생성하고 실행하는 메인 스레드
public class WaitNotifyEx {
	public static void main(String[] args) {
		DataBox databox = new DataBox();
		
		ConsumerThread consumer = new ConsumerThread(databox);
		ProducerThread producer = new ProducerThread(databox);
		
		
		consumer.start();
		producer.start();
	}
}

실행 모습

ProducerThread가 생성한 데어터 : Data-1
ConsummerThread가 읽은 데이터 : Data-1
ProducerThread가 생성한 데어터 : Data-2
ConsummerThread가 읽은 데이터 : Data-2
ProducerThread가 생성한 데어터 : Data-3
ConsummerThread가 읽은 데이터 : Data-3

스레드의 안전한 종료(stop, interrupt)

스레드는 자신의 fun() 메소드가 모두 실행되면 자동적으로 종료되지만, 경우에 따라서는 실행 중인 스레드를 즉시 종료해야한다.

(비권유) stop

방법 1은 stop 플래그를 사용하는 것이다.
스레드는 run() 메소드가 끝나면 자동적으로 종료되므로, run 메소드가 정상적으로 종료되록 stop 플래그를 이용해서 유도하는 것이다.

비권유되는 이유는 stop() 메소드로 스레드를 갑자기 종료하게 된다면 스레드가 사용 중이던 자원들이 불안전한 상태로 남겨지기 때문이다

예제

public class StopFlagEx {
	public static void main(String[] args) {
		PrintThread1 pt1 = new PrintThread1();
		pt1.start();
		
		// 1초 뒤에 다음 줄의 코드가 실행될 것이다
		try { PrintThread1.sleep(1000); }
		catch (InterruptedException e) {	}
		
		pt1.setStop(true);
	}
}


class PrintThread1 extends Thread {
	private boolean stop;
	
	public void setStop(boolean stop) {
		this.stop = stop;
	}
	@Override
	public void run() {
		while(!stop) {
			System.out.println("실행 중");
		}
		// stop이 true 될 때
		System.out.println("자원 정리");
		System.out.println("실행 종료");
	}
}

실행 모습

…
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
자원 정리
실행 종료

interrupt() 메소드를 이용하는 방법

스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시켜 run() 메소드를 정상 종료 시키는 것이다

예제

// 1. 일시정지 코드인 Thread.sleep(1)으로 exception 발생시키기
public class InterruptEx {
	public static void main(String[] args) {
		// 다형성 부모 클래스로 객체화해봄
		Thread pt2 = new PrintThread2();
		pt2.start();
		
		try {
			// 1초뒤에 다음 줄 코드를 실행시켜 줄것이다.
			Thread.sleep(1000);
		}catch (InterruptedException e) {	}
		
		// 스레드를 종료시키기 위해 interruptedException발생
		// 스레드가 일시정지 상태가 되면 예외가 발생한다는 것이고
		// 따라서 일시정지 상태가 되지 않으면 interrupt() 메소드 호출은 아무 의미가 없다
		pt2.interrupt();
	}
}


class PrintThread2 extends Thread {
	@Override
	public void run() {
		try {
			while(true) {
				System.out.println("실행 중");
				// InterruptedException 발생
				// 스레드가 일시 정지 상테에 있을 때 예외를 발생시키는 역할을 한다
				Thread.sleep(1);
			}
			// Exception 받아줄 catch
		}catch (InterruptedException e) {	}
		
		System.out.println("자원 정리");
		System.out.println("실행 종료");
	}
}


-----------------------------------------------------------------------

// 2. Thread.interrupted()를 사용해서 interrupt()가 호출되었는지 확인
public class InterruptEx02 {
	public static void main(String[] args) {
		// 다형성 부모 클래스로 객체화해봄
		PrintThread3 pt2 = new PrintThread3();
		pt2.start();

		try {
			// 3초뒤에 실행이 다음줄 코드가 실행되도록 해줄것이다
			Thread.sleep(2000);
		} catch (InterruptedException e) {
		}

		pt2.interrupt();
	}
}

class PrintThread3 extends Thread {
	@Override
	public void run() {
		// 일시정지 코드인 Thread.sleep(1)을 사용하지 않고,
		// Thread.interrupted()를 사용하여
		// 메인에서 interrupt()가 호출 되었는지 확인하고 while문을 빠져나간다
		while(true) {
			System.out.println("실행 중");
			// 스레드에서 인터럽트 상태를 판별해낸다
			// interrupted가 발생되면 true를 반환하고 빠져나오도록 한 코드
			if (Thread.interrupted()) {
				break;
			}
		}

		System.out.println("자원 정리");
		System.out.println("실행 종료");
	}
}

실행 모습

…
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
자원 정리
실행 종료

sleep을 사용한 방법이나 Thread.interrupted()를 사용해 interrupt() 호출 여부를 확인하는 방법이나 출력 모습은 동일하다.


데몬 스레드

: 주 스레드의 작업을 돕는 보조적인 역할 수행 스레드
: 주 스레드가 종료되면 데몬 스레드는 강제적으로 자동 종료된다.

스레드를 데몬 스레드로 만들기
: 주 스레드가 데몬이 될 스레드의 setDaemon(true) 호출
: 반드시 Start() 메소드 호출 전에 setDaemon(true)를 호출해야 한다
-> 그렇지 않으면 예외 발생

현재 실행중인 스레드가 데몬 스레드인지 구별법
: isDaemon() 메소드의 리턴값을 조사한다
-> true가 리턴오면 데몬스레드인 것

사용 예제

다음 예제는 0.5초 주기로 save() 메소드를 자동 호출하도록 AutoSaveThread를 작성하고, 메인 스레드가 3초 후 종료되면 AutoSaveThread도 같이 종료 되도록 만들었다.

// AutoSaveThread
public class AutoSaveThread extends Thread{
	public void save() {
		System.out.println("작업 내용을 저장함");
	}
	
	@Override
	public void run() {
		while(true) {
			try {
				// 0.5초 뒤에 다음 코드를 실행한다
				Thread.sleep(500);
			}catch (InterruptedException e) {
				break;
			}
			save();
		}
	}
}


// 메인 스레드가 실행하는 코드
public class DaemonEx {
	public static void main(String[] args) {
		AutoSaveThread ast = new AutoSaveThread();
		// AutoSaveThread를 데몬 스레드로 만든다
		ast.setDaemon(true);
		ast.start();
		
		try {
			// 3초 뒤에 다음 코드를 실행한다
			Thread.sleep(3000);
		}catch (InterruptedException e) { }
		
		System.out.println("메인 스레드 종료");
	}
}

실행 결과

작업 내용을 저장함
작업 내용을 저장함
작업 내용을 저장함
작업 내용을 저장함
작업 내용을 저장함
메인 스레드 종료

AutoSaveThread를 데몬 스레드로 set(true)를 해주고 start() 하니
메인이 3초 잠들어 있는 동안 AutoSaveThread가 0.5초 간격으로 실행되었다. 3초가 지난 후 메인의 다음 코드가 실행되면서 스레드는 종료되는 모습을 확인 할 수 있었다.

set데몬을 해주지 않았을 경우에는 메인이 종료되어도 스레드가 실행되면서 아래와 같은 결과 값이 출력된다.

작업 내용을 저장함
작업 내용을 저장함
작업 내용을 저장함
작업 내용을 저장함
작업 내용을 저장함
메인 스레드 종료
작업 내용을 저장함
작업 내용을 저장함
…

스레드 그룹

: 관련된 스레드를 묶어서 관리할 목적으로 이용된다. 우리가 생성하는 작업 스레드는 대부분 main 스레드가 생성하므로 기본적으로 main 스레드 그룹에 속하게 된다

스레드 그룹 이름 얻기

다음 예제는 현재 실행하고 있는 스레드의 이름과 데몬 여부 그리고 속한 스레드 그룹 명이 무엇인지 출력한다.

public class A_ThreadInfoEx {
	public static void main(String[] args) {
		AutoSaveThread autoSave = new AutoSaveThread();
		autoSave.setName("AutoSaveThread");
		autoSave.setDaemon(true);
		autoSave.start();
		
		// 자료타입은 Map이고
		// 키, 값 = Thread, StackTraceElement[]
		Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
		// Map 이라는 클래스에서 키에 해당하는 값만 다 얻어와
		// 변수 threads에 넣어주기
		Set<Thread> threads = map.keySet();
		
		// 향샹된 for 문 사용
		// Thread를 하나씩 가져와 돌림
		for(Thread thread : threads) {
			// 3항 연산자
			// 만약 Daemon이라면 (데몬) 출력 아니라면 (주) 출력
			System.out.println("Name : " + thread.getName() +
					((thread.isDaemon())?"(데몬)" : "(주)"));
			System.out.println("\t소속그룹 : " + thread.getThreadGroup().getName());
			System.out.println();
		}
	}
}

class AutoSaveThread extends Thread{
	public void save() {
		System.out.println("작업 내용을 저장함");
	}
	
	@Override
	public void run() {
		while(true) {
			try {
				// 1뒤에 다음 코드를 실행한다
				Thread.sleep(500);
			}catch (InterruptedException e) {
				break;
			}
			save();
		}
	}
}

실행 모습

Name : AutoSaveThread(데몬)
	소속그룹 : main
Name : Finalizer(데몬)
	소속그룹 : system
Name : Attach Listener(데몬)
	소속그룹 : system
Name : Reference Handler(데몬)
	소속그룹 : system
Name : Signal Dispatcher(데몬)
	소속그룹 : system
Name : main(주)
	소속그룹 : main

스레드 그룹 생성

스레드 그룹을 명시적으로 만들고 싶다면 다음 생성자 중 하나를 이용해서 만들면 된다.

ThreadGroup tg = new ThreadGroup(String name);
ThreadGroup tg = new ThreadGroup(ThreadGroup parent, String name);

생성시 부모 스레드 그룹을 지정하지 않으면 현재 스레드가 속한 그룹의 하위 그룹으로 생성된다.

새로운 스레드 그룹을 생성한 후, 이 그룹에 스레드를 포함시키려면 Thread 객체를 생성할 때 생성자 매개값으로 스레드 그룹을 지정하면 된다. 스레드 그룹을 매개값으로 가지는 Thread 생성자는 다음 네가지가 있다.

Thread t = new Thread(ThreadGroup group, Runnable target);
Thread t = new Thread(ThreadGroup group, Runnable target, String name);
Thread t = new Thread(ThreadGroup group, Runnable target, String name, long stackSize);
Thread t = new Thread(ThreadGroup group, String name);

Runnable 타입의 target은 Runnable 구현 객체를 말하며, String 타입의 name은 스레드의 이름이다. 그리고 long 타입의 stackSize는 JVM이 이 스레드에 할당할 stack 크기이다.


스레드 그룹의 일괄 interrupt()

스레드를 그룹에 포함시키면, 스레드 그룹에서 제공하는 interrupt()메소드를 이용해 그룹내 포함된 모든 스레드들을 일괄 interrupt할 수 있다.
단, interrupt() 메소드를 호출만 할 뿐 개별 스레드에서 발생하는 예에에 대해 예외처리를 해주지 않는다.

따라서 안전한 종료를 위해서는 개별 스레드가 예외 처리를 해야한다.

ThreadGrop이 가지고 있는 주요 메소드

예제

  1. 스레드 그룹을 생성하고, 정보를 출력해본다.
  2. 3초후 스레드 그룹의 interrupt()메소드를 호출해 스레드 그룹에 포함된 모든 스레드들을 종료시킨다.
public class B_ThreadGropEx {
	public static void main(String[] args) {
		// import하지 않아도 되는 기본 생성자이며
		// 매개변수로 스트링을 넘겼다.
		ThreadGroup myGroup = new ThreadGroup("myGroup");
		// myGroup에 두 스레드를 포함시킨다
		WorkThread workThreadA = new WorkThread(myGroup, "workThradA");
		WorkThread workThreadB = new WorkThread(myGroup, "workThradB");
		
		workThreadA.start();
		workThreadB.start();
		
		// 현재 스레드 상태(currentThread()) 를
		// 스레드 그룹(getThreadGroup()) 으로 리턴받을 것이다
		System.out.println("[ main 스레드 그룹이 list() 메소드 출력 내용 ]");
		ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
		// 받은 것들을 list()로 출력할 것이다
		// [ 이름, 우선순위, 그룹]
		mainGroup.list();
		
		System.out.println();
		
		try {
			// 3초 후 다음 코드 출력
			Thread.sleep(4000);
		}catch (InterruptedException e) {	}
		
		// 그룹으로 묶인 스레드들을 한번에 interrupt() 해준다
		System.out.println("[ myGroup 스레드 그룹의 interrupt() 메소드 호출 ]");
		myGroup.interrupt();
	}
}

class WorkThread extends Thread {
	public WorkThread(ThreadGroup threadGroup, String threadName) {
		// 부모인 Thread에 넘겨줄 것이다
		// 스레드 그룹과 스레드 이름을 설정
		super(threadGroup,threadName);
	}
	@Override
	public void run() {
		while(true) {
			try {
				// 1초 후 다음 코드를 실행하겠다
				Thread.sleep(1000);
			}catch (InterruptedException e) {
				// 예외처리 발생시 while문을 빠져나와 스레드를 종료시킨다
				System.out.println(getName() + " interrupted");
				break;
			}
		}
		System.out.println(getName() + " 종료됨");
	}
}

실행 결과

[ main 스레드 그룹이 list() 메소드 출력 내용 ]
java.lang.ThreadGroup[name=main,maxpri=10]
    Thread[main,5,main]
    java.lang.ThreadGroup[name=myGroup,maxpri=10]
        Thread[workThradA,5,myGroup]
        Thread[workThradB,5,myGroup]
[ myGroup 스레드 그룹의 interrupt() 메소드 호출 ]
workThradB interrupted
workThradB 종료됨
workThradA interrupted
workThradA 종료됨

이해하기 어렵다..ㅠ

0개의 댓글