본문 바로가기
Java/Java

쓰레드) 쓰레드 컨트롤 (sleep, join, interrupt), Daemon 쓰레드

by 박채니 2022. 4. 3.

안녕하세요, 코린이의 코딩 학습기 채니 입니다.

 

개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.


쓰레드 컨트롤

- 실행 중인 쓰레드의 상태를 변경하는 것

 

☞ Thread.sleep

- 쓰레드를 ( ~ ) 밀리초만큼 일시정지 시킴

public void test1() {
	Thread th1 = new Thread(new SleepThread('+'));
	Thread th2 = new Thread(new SleepThread('|'));
	th1.start();
	th2.start();
}
	
public static class SleepThread implements Runnable {
	private char ch;
		
	public SleepThread(char ch) {
		this.ch = ch;
	}
		
	@Override
	public void run() {
		for(int i = 0; i < 100; i++) {
			System.out.print(ch);
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

쓰레드가 실행된 후 ch를 출력하고 0.5초간 일시정지가 되었다가 다시 실행됩니다.

따라서 콘솔창에 0.5초의 간격을 두면서 "+"와 "|"가 출력이 됩니다.

 

각 쓰레드마다 일시정지 시간을 다르게 해주고 싶다면?

public void test1() {
	Thread th1 = new Thread(new SleepThread('+', 300));
	Thread th2 = new Thread(new SleepThread('|', 500));
	th1.start();
	th2.start();
}
	
public static class SleepThread implements Runnable {
	private char ch;
	private long millis;
		
	public SleepThread(char ch, long millis) {
		this.ch = ch;
		this.millis = millis;
	}
		
	@Override
	public void run() {
		for(int i = 0; i < 100; i++) {
			System.out.print(ch);
			try {
				Thread.sleep(millis);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

 

쓰레드 이름 지정

th1.setName("[Thread1]");
th2.setName("[Thread2]");

 

현재 쓰레드 이름 출력

System.out.println(Thread.currentThread().getName());

 

public void test1() {
	Thread th1 = new Thread(new SleepThread('+', 30));
	Thread th2 = new Thread(new SleepThread('|', 50));
	th1.setName("[Thread1]");
	th2.setName("[Thread2]");
	th1.start();
	th2.start();
}
	
public static class SleepThread implements Runnable {
	private char ch;
	private long millis;
	
	public SleepThread(char ch, long millis) {
		this.ch = ch;
		this.millis = millis;
	}
		
	@Override
	public void run() {
		for(int i = 0; i < 100; i++) {
			System.out.print(ch);
			try {
				Thread.sleep(millis);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println(Thread.currentThread().getName());	//현재쓰레드의 이름
	}
}

Thread.currentThread()를 통해서 현재 쓰레드에 접근하여 .getName() 쓰레드 이름을 가져와 출력하였습니다.

setName()으로 쓰레드의 이름을 지정해주지 않으면 쓰레드 이름의 기본값은 Thread-0, Thread-1 ... 이 됩니다.

 


구구단 쓰레드
- 8명의 학생(Thread)에게 구구단 각 단을 외워볼 수 있도록 한다.

- 각 쓰레드의 일시정지는 0 ~ 1000 밀리초로 랜덤하게 지정한다.
[2단] 2 * 1 = 2

[2단] 2 * 2 = 4

[3단] 3 * 1 = 3

[5단] 5 * 1 = 5

...

GugudanThread 하나의 클래스만 생성할 것!

public void test2() {
	for(int dan = 2; dan < 10; dan++) {
		Thread th = new Thread(new GugudanThread(dan), dan+"단");
		th.start();
	}
}
	
public static class GugudanThread implements Runnable {
	private int dan;
		
	public GugudanThread(int dan) {
		this.dan = dan;
	}
		
	@Override
	public void run() {
		for(int i = 1; i < 10; i++) {
			System.out.printf("[%s] %d * %d = %d\n", Thread.currentThread().getName(), 
            						dan, i, dan*i);
			try {
				Thread.sleep((long)(Math.random()*1001));
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}	
	}
}

@콘솔출력값
[3단] 3 * 1 = 3
[9단] 9 * 1 = 9
[7단] 7 * 1 = 7
[8단] 8 * 1 = 8
[6단] 6 * 1 = 6
[4단] 4 * 1 = 4
[2단] 2 * 1 = 2
[5단] 5 * 1 = 5
[6단] 6 * 2 = 12
[3단] 3 * 2 = 6
[7단] 7 * 2 = 14
[2단] 2 * 2 = 4
[6단] 6 * 3 = 18
...
...

setName()을 통해서 쓰레드의 이름을 설정해줄 수도 있지만, 생성자를 통해서도 이름을 지정해줄 수 있습니다.


☞ Daemon 쓰레드

- 리눅스/유닉스 계열의 OS에서 백그라운드에서 작동하는 프로그램

- 일반 쓰레드의 종속적으로 작동 (일반 쓰레드가 종료되면 데몬 쓰레드도 종료)

public static void main(String[] args) {
	ThreadStatusStudy study = new ThreadStatusStudy();
	study.test3();	
}

public void test3() {
	Thread countDown = new Thread(new CountDown(10), "[카운트다운]");
	countDown.start();
}
	
public static class CountDown implements Runnable {
	private int num;
		
	public CountDown(int num) {
		this.num = num;
	}
		
	@Override
	public void run() {
		for(int i = num; i >= 0; i--) {
			System.out.println(i);
		try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println(Thread.currentThread().getName() + " 쓰레드 종료!");
	}
}

@콘솔출력값
10
9
8
7
6
5
4
3
2
1
0
[카운트다운] 쓰레드 종료!

main메소드에서 test3()을 호출하고, test3()메소드에서 쓰레드를 start() 시켰습니다.

마찬가지로 10, 9, 8 ... 0 쓰레드 종료가 출력된 것을 확인할 수 있습니다.

 

main 쓰레드는 언제 종료되는 지 확인해보겠습니다.

public static void main(String[] args) {
	ThreadStatusStudy study = new ThreadStatusStudy();
	study.test3();
	//main 쓰레드 종료확인
	System.out.println(Thread.currentThread().getName() + " 쓰레드 종료!");
}

public void test3() {
	Thread countDown = new Thread(new CountDown(10), "[카운트다운]");
	countDown.start();
}
	
public static class CountDown implements Runnable {
	private int num;
		
	public CountDown(int num) {
		this.num = num;
	}
		
	@Override
	public void run() {
		for(int i = num; i >= 0; i--) {
			System.out.println(i);
		try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println(Thread.currentThread().getName() + " 쓰레드 종료!");
	}
}

@콘솔출력값
main 쓰레드 종료!
10
9
8
7
6
5
4
3
2
1
0
[카운트다운] 쓰레드 종료!

test()을 호출하고 리턴이 되면서 쓰레드가 종료되어 "main 쓰레드 종료!"를 출력하고 countDown이 이뤄진 후 카운트다운 쓰레드가 종료되었습니다.

 

그렇다면 일반 쓰레드가 종료되면 같이 종료되는 데몬 쓰레드는 어떨까요?

public static void main(String[] args) {
	ThreadStatusStudy study = new ThreadStatusStudy();
	study.test3();
	//main 쓰레드 종료확인
	System.out.println(Thread.currentThread().getName() + " 쓰레드 종료!");
}

public void test3() {
	Thread countDown = new Thread(new CountDown(10), "[카운트다운]");
	//Daemon 쓰레드로 설정
	countDown.setDaemon(true);
	countDown.start();
}
	
public static class CountDown implements Runnable {
	private int num;
		
	public CountDown(int num) {
		this.num = num;
	}
		
	@Override
	public void run() {
		for(int i = num; i >= 0; i--) {
			System.out.println(i);
		try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println(Thread.currentThread().getName() + " 쓰레드 종료!");
	}
}

@콘솔출력값
main 쓰레드 종료!
10

countDown 쓰레드를 setDaemon(true)를 통해 데몬 쓰레드로 지정해주었습니다.

그랬더니 main()에서 test3()을 호출하고 리턴 되었을 때 main 쓰레드가 종료되었고, 종료됨과 동시에 데몬쓰레드인 CountDown 쓰레드가 종료되어 이후 코드를 실행하지 않게 되었습니다.

 

확실히 확인하기 위하여 엔터를 누르면 메인쓰레드가 종료되도록 해보았습니다.

public static void main(String[] args) {
	ThreadStatusStudy study = new ThreadStatusStudy();
	study.test3();
	//main 쓰레드 종료확인
	System.out.println(Thread.currentThread().getName() + " 쓰레드 종료!");
}

public void test3() {
	Thread countDown = new Thread(new CountDown(10), "[카운트다운]");
	//Daemon 쓰레드로 설정
	countDown.setDaemon(true);
	countDown.start();
	
	//엔터를 누르지않으면 리턴되지 않으므로 main쓰레드는 종료되지 않음
	System.out.println("엔터를 누르면 메인 쓰레드는 종료됩니다.");
	new Scanner(System.in).nextLine();
}
	
public static class CountDown implements Runnable {
	private int num;
		
	public CountDown(int num) {
		this.num = num;
	}
		
	@Override
	public void run() {
		for(int i = num; i >= 0; i--) {
			System.out.println(i);
		try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println(Thread.currentThread().getName() + " 쓰레드 종료!");
	}
}

@콘솔출력값
엔터를 누르면 메인 쓰레드는 종료됩니다.
10
9
8
7

main 쓰레드 종료!

엔터를 누르지 않으면 Scanner의 nextLine() 입력 값을 기다리고 있기 때문에 리턴 되지 않아 main 쓰레드가 종료되지 않을 것입니다.

하지만 엔터를 누름과 동시에 메소드가 리턴 되어 main 쓰레드가 종료될 것이고, 그렇다면 데몬 쓰레드인 CountDown 쓰레드도 종료되어 이후 값을 출력하지 않고 끝나게 되겠죠.


☞ join

- 특정 쓰레드가 종료되기까지 대기(waiting) 되었다 진행하는 메소드

- 특정시간만 대기도 가능

public static void main(String[] args) {
		ThreadStatusStudy study = new ThreadStatusStudy();
		study.test4();
		
		System.out.println(Thread.currentThread().getName() + " 쓰레드 종료!");
	}
	
	public void test4() {
		Thread cd = new Thread(new CountDown(10), "[카운트다운]");
		cd.start();
		
		try {
			//현재(호출한) 쓰레드가 cd쓰레드를 기다림
			cd.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	public static class CountDown implements Runnable {
		private int num;
		
		public CountDown(int num) {
			this.num = num;
		}
		
		@Override
		public void run() {
			for(int i = num; i >= 0; i--) {
				System.out.println(i);
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName() + " 쓰레드 종료!");
		}
	}
    
@콘솔출력값
10
9
8
7
6
5
4
3
2
1
0
[카운트다운] 쓰레드 종료!
main 쓰레드 종료!

이번에는 현재(호출한) 쓰레드 즉, main 쓰레드가 countDown 쓰레드가 종료될 때까지 waiting(대기) 하도록 하였습니다.

join()으로 통해서 쓰레드를 대기 시킬 수 있는데요,

그 결과 test4() 호출 후 리턴 되면 종료되던 main 쓰레드가 join()으로 인해서 countDown 쓰레드가 종료될 때까지 기다렸다가 countDown 쓰레드가 종료됨과 동시에 같이 종료되는 것을 확인할 수 있습니다.

 

이렇게 쓰레드 자체를 대기하는 것도 가능하지만 대기 시간을 정할 수도 있는데요,

public class ThreadStatusStudy {
	public static void main(String[] args) {
		ThreadStatusStudy study = new ThreadStatusStudy();
//		study.test1();
//		study.test2();
//		study.test3();
		study.test4();
		
		System.out.println(Thread.currentThread().getName() + " 쓰레드 종료!");
		
	}
	
	public void test4() {
		Thread cd = new Thread(new CountDown(10), "[카운트다운]");
		cd.start();
		
		try {
			//현재(호출한) 쓰레드가 cd쓰레드를 기다림
//			cd.join();
			cd.join(3000);	//3초 뒤에 종료!
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	public static class CountDown implements Runnable {
		private int num;
		
		public CountDown(int num) {
			this.num = num;
		}
		
		@Override
		public void run() {
			for(int i = num; i >= 0; i--) {
				System.out.println(i);
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName() + " 쓰레드 종료!");
		}
	}
    
@콘솔출력값
10
9
8
main 쓰레드 종료!
7
6
5
4
3
2
1
0
[카운트다운] 쓰레드 종료!

cd.join(3000) 즉, 현재 쓰레드는 3초 동안 waiting (대기) 상태이고 3초 뒤에 종료가 된다는 것입니다.

출력값을 보면 정확히 3초 뒤에 main 쓰레드가 종료되는 것을 확인할 수 있습니다.


☞ interrupt

- 특정 쓰레드를 종료/중지 시키는 메소드

- InterruptedException을 발생시켜 흐름을 분기처리

public class ThreadStatusStudy {
	public static void main(String[] args) {
		ThreadStatusStudy study = new ThreadStatusStudy();
		study.test5();
		
		System.out.println(Thread.currentThread().getName() + " 쓰레드 종료!");
	}
	
	public void test5() {
		Thread cd = new Thread(new CountDown(10), "[카운트다운]");
		cd.start();
		
		System.out.println("엔터를 누르면 카운트다운이 멈춥니다.");
		new Scanner(System.in).nextLine();
		cd.interrupt();	//현재 쓰레드에서 cd쓰레드에 interrupt!!
	}
	
	public static class CountDown implements Runnable {
		private int num;
		
		public CountDown(int num) {
			this.num = num;
		}
		
		@Override
		public void run() {
			for(int i = num; i >= 0; i--) {
				System.out.println(i);
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName() + " 쓰레드 종료!");
		}
	}
    
@콘솔출력값
엔터를 누르면 카운트다운이 멈춥니다.
10
9
8
7

main 쓰레드 종료!
java.lang.InterruptedException: sleep interrupted
6
	at java.base/java.lang.Thread.sleep(Native Method)
	at com.ce.ThreadStatusStudy$CountDown.run(ThreadStatusStudy.java:40)
	at java.base/java.lang.Thread.run(Thread.java:829)
5
4
3
2
1
0
[카운트다운] 쓰레드 종료!

현재 쓰레드에서 cd쓰레드에 interrupt! 하게 하였습니다.

엔터를 눌렀더니 interrupt()를 만나 현재(main) 쓰레드는 종료된 후 InterruptedException 을 발생시켰고 이미 예외처리를 했기 때문에 예외 메세지를 띄우고 (예외처리) 계속 카운트 다운을 출력하는 것을 확인할 수 있습니다.

 

public class ThreadStatusStudy {
	public static void main(String[] args) {
		ThreadStatusStudy study = new ThreadStatusStudy();
		study.test5();
		
		System.out.println(Thread.currentThread().getName() + " 쓰레드 종료!");
	}
	
	public void test5() {
		Thread cd = new Thread(new CountDown(10), "[카운트다운]");
		cd.start();
		
		System.out.println("엔터를 누르면 카운트다운이 멈춥니다.");
		new Scanner(System.in).nextLine();
		cd.interrupt();	//현재 쓰레드에서 cd쓰레드에 interrupt!!
	}
	
	public static class CountDown implements Runnable {
		private int num;
		
		public CountDown(int num) {
			this.num = num;
		}
		
		@Override
		public void run() {
			for(int i = num; i >= 0; i--) {
				System.out.println(i);
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					break;
				}
			}
			System.out.println(Thread.currentThread().getName() + " 쓰레드 종료!");
		}
	}
    
@콘솔출력값
엔터를 누르면 카운트다운이 멈춥니다.
10
9
8

main 쓰레드 종료!
[카운트다운] 쓰레드 종료!

interrupt가 되면 아예 카운트다운 조차 종료될 수 있게 예외처리에 break;를 하여 카운트다운도 같이 종료될 수 있게 하였습니다.