사용자는 미디어 플레이어에서 동영사을 보다가 일시 정지 시킬 수도 있고, 종료 시킬 수도 있다
일시 정지는 조금 후 다시 동영상을 보겠다는 의미이므로
미디어 플레이어는 동영상 스레드를 일시 정지 상태로 만들어야 한다
그리고 종료는 더 이상 동영상을 보지 않겠다는 의미 이다
미디어 플레이어는 스레드를 종료 상태로 만들어야 한다.
이와 같이 실행 중인 스레드의 상태를 변경하는 것을 스레드 상태 제어라고한다.
멀티 스레드 프로그램을 만들기 위해서는 정교한 스레드 상태 제어가 필요한데,
상태 제어가 잘못되면 프로그램은 불안정해져서 먹통이 되거나 다운된다.
멀티 스레드 프로그래밍이 어렵다고 하는 이유는 바로 여기에 잇다.
스레드를 잘 사용하면 약이 되지만, 잘못하용하면 치명적인 프로그램 버그가 되기 때문에
스레드를 정확하게 제어하는 방법을 잘 알고 있어야 한다.
스레드 제어를 제대로 하기 위해서는 스레드의 상태 변화를 가져오는 메소드들을 파악하고 있어야 한다
다음 그림은 상태 변화를 가져오는 메소드의 종류를 보여 준다.
위 에서 취소선을 가진 메소드는 스레드의 안전성을 해친다고 하여 더 이상 사용하지 않도록 권장하고 있다
메소드 | 설명 |
interrupt() | 일시 정지 상태의 스레드에서 InterruptedException 예외를 발생시켜, 예외 처리 코드(catch) 에서 실행 대기 상태로 가거나 종료 상태로 갈수 있도록 한다 |
notify() notifyAll() |
동기화 블록 내에서 wati() 메소드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만든다 |
resume() | suspend() 메소드에 의해 일시 정지 상태에 있는 스레드들 실행 대기 상태로 만든다. -Deprecated(대신 notify(), notifyAll() 사용 ) |
sleep(long millis) sleep(long millis, int nanos) |
주어진 시간 동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. |
join() join(long millis) join(long millis, int nanos) |
join() 메소드를 호출한 스레드는 일시 정지 상태가 된다. 실행 대기 상태로 가려면, join() 메소드를 멤버로 가지는 스레드가 종료 되거나, 매개값으로 주어진 시간이 지나야 한다. |
wait() wait(long millis) wait(long millis, int nanos) |
동기화(synchronized) 블록 내에서 스레드를 일시 정지 상태로만든다. 매개값으로 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. 시간이 주어지지 않으면 notify(), notifyAll()메소드에 의해 실행 대기 상태로 갈수 있다. |
suspend() | 스레드의 일시 정지 상태로 만든다. resume() 메소드를 호출하면 다시 실행 대기 상태가 된다. -Deprecate(대신 wait() 사용) |
yield() | 실행 중에 우선위가 동일한 다른 스레드에게 실행을 양보하고 실행 대기 상태가 된다 |
stop() | 스레드를 즉시 종료시킨다. -Dprcated |
위 표에서 wait(), notify() , notifyAll() 는 Object 클래스의 메소드이고,
그 이외의 메소드는 모두 Thread 클래스의 메소드들이다 wait(), notify(), notifyAll() 메소드의 사용 방법은 스레드의 동기화에서 자세히 살펴 보자
주어진 시간동안 일시 정지 (sleep())
실행 중인 스레드를 일정 시간 멈추게 하고 싶다면 Thread 클래스의 정적 메소드인 sleep()을 사용
Thread.sleep() 메소드를 호출한 스레드는 주어진 시간 동안 일시 정지 상태가 되고, 다시 실행 대기 상태로 돌아간다.
try{
Thread.sleep(1000);
} catch(InterruptedException e ){
// interrupt 메소드가 호출되면 실행
}
매개값에는 얼마동안 일시 정지 상태로 있을 것인지 , 밀리세컨드(1/1000) 단위로 시간을 주면 된다
위와 같이 1000이라는 값을 주면 스레드는 1초가 경과할 동안 일시 정지 상태로 있게 된다.
일시정지 상태에서 주어진 시간이 되기 전에 interrupt() 메소드가 호출되면 InterruptedException이 발생 하기 때문에 예외처리가 필요하다.
import java.awt.ToolKit;
public class SleepExample {
public static void main (String[] args){
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i = 0; i<10; i++) {
toolkit.beep();
try{
Thread.sleep(3000);
} catch(InterruptedException e){
}
}
}
}
Thread.sleep(3000)은 메인 스레드를 3초동안 일시 정지 상태로 보내고,
3초가 지나면 다시 실행 준비 상태로 돌아오록 했다.
다른 스레드에게 실행 양보(yiled())
스레드가 처리하는 작업은 반복적인 실행을 위해 for문이나 while 문을 포함하는 경우가 많다.
가끔은 이 반복문들이 무의미한 반복을 하는 경우가 있다
public void run(){
while(true){
if(work){
System.out.println("ThreadA 작업 내용 ")
}
}
}
스레드가 시작되어 run() 메소드를 실행하면 while(true) { } 블록을 무한 반복 실행한다.
만약 work의 값이 false라면 그리고 work의 값이 false 에서 true로 변경되어 시점이 불명확하다면,
while 문은 어떠한 실행문도 실행하지 않고 무의미한 반복을 한다.
이것보다는 다른 스레드 에게 실행을 양보
하고 자신은 실행 대기 상태로 가는 것이 전체 프로그램 성능에 도움이 된다
이런 기능을 위해서 스레드는 yield() 메소드를 제공하고 있다. yield() 메소드를 호출한 스레드는 실행 대기 상태로
돌아가고 동일한 우선순위 또는 노은 우선순위를 갖는 다른 스레드가 실행 기회를 가질수 있도록 해준다
의미 없는 반복을 줄이기 위해 yield() 메소드를 호출해서 다른 스레드에게 실행 기회를 주도록 수정
public void run(){
while(true){
if(work){
System.out.println("ThreadA 작업 내용");
} else {
Thread.yield();
}
}
}
다음 예제에서는 처음 실행 후 3초 동안은 ThreadA 와 ThreadB 가 번갈아 가며 실행 된다.
3초뒤에 메인 스레드가 ThreadA의 work 필드를 false로 변경함으로써 ThreadA는 yield() 메소드를 호출
따라서 이후 3초 동안에는 ThreadB가 더 많은 실행 기회를 얻게 된다.
메인 스레드는 3초 뒤에 다시 ThreadA의 work 필드를 true 로 변경해서
ThreadA와 ThreadB가 번갈아 가며 시행하도록 한다
마지막으로 메인 스레드는 3초뒤 ThreadA 와 ThreadB의 stop 필드를 true 로 변경해서
두스레드 반복작업을 중지하고 종료하도록 한다.
public class YieldExample {
public static void main(String[] args){
ThreadA threadA = new ThreadA();
ThreadB threadB = new ThreadB();
threadA.start();
threadB.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e){
}
threadA.work = false;
try {
Thread.sleep(3000);
} catch (InterruptedException e){
}
threadA.work = true;
try{
Thread.sleep(3000);
} catch (InterruptedException e){
}
threadA.stop = true;
threadB.stop = true;
}
}
public class ThreadA extends Thread{
public boolean stop = false;
public boolean work = true;
public void run(){
while(!stop){
if(work){
System.out.println("ThreadA 작업 내용"); // stop 의 true가 되면 while문 종료
}
} else {
Thread.yield(); // work가 false 가 되면 다른 스레드에게 실행 양보
}
System.out.println("ThreadA 종료");
}
}
public class ThreadB extends Thread{
public boolean stop = false;
public boolean work = true;
public void run(){
while(!stop){
if(work){
System.out.println("ThreadB 작업 내용"); // stop 의 true가 되면 while문 종료
}
} else {
Thread.yield(); // work가 false 가 되면 다른 스레드에게 실행 양보
}
System.out.println("ThreadB 종료");
}
}
다른 스레드의 종료를 기다림 (join())
스레드는 다른 스레드와 독립적으로 실행하는 것이 기본이지만 다른 스레드가 종료 될때까지 기다렸다가
실행해야 하는 경우가 발생 할 수도 있다.
예를 들어 계산 작업을 하는 스레드가 모든 계산 작업을 마쳤을 때, 계산 결과값을 받아 이용하는 경우가 이에 해당
이런 경우를 위해서 Thread는 join() 메소드를 제공하고 있다.
ThreadA가 ThreadB의 join() 메소드를 호출하면 ThreadA는 ThreadB가 종료 할때 까지 일시 정지 상태가 된다.
ThreadB 의 join() 메소드를 호출하면 ThreadA는 ThreadB 가 종료 할때 까지 일시 정지 상태가 된다.
ThreadB의 run() 메소드가 종료되면 비로소 ThreadA는 일시 정지에서 풀려 다음 코드를 실행하게 된다.
public 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;
}
}
}
public class JoinExample{
public static void main(String[] args){
SumThread sumThread = new SumThread();
sumThread.start();
try{
sumThread.join(); // sumThread가 종료할 때까지 메인 스레드를 일시 정지 시킴
} catch (InterruptedException e){
}
System.out.println("1-100 합: " + sumThread.getSum());
}
}
JoinExample 클래스의 sumThread.join 라인을 주석 처리하고 실행하면 1~100까지의 합은 0이 나오게 된다
/// 컴퓨터 성능에 따라 다른 값이 출력 될 수 있다.
sumThread가 계산 작업을 완료하지 않은 상태에서 합을 먼저 출력
스레드가 협업 (wait() , notifym notifyAll())
경우에 따라서는 두 개의 스레드를 교대로 번갈아가며 실행해야 할 경우가 있다
정확한 교대 작업이 필요할 경우 , 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고,
자신은 일시 정지 상태로 만드는 것이다.
이 방법의 핵심은 공유 객체에 있다.
공유 객체는 두 스레드가 작업 할 내용을 각각 동기화 메소드로 구분
한 스레드가 작업을 완료하면 notify() 메소드를 호출해서
일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고
자신은 두번 작업을 하지 않도록 wait() 메소드를 호출하여 일시 정지 상태로 만든다.
wait() 대신 wait(long timeou) 이나 , wait(long timeout, int nanos)를 사용하면
notify()를 호출하지 않아도 지정된 시간이 지나면 스레드가 자동적으로 실행 대기 상태가 된다.
notify() 메소드와 동일한 역할을 하는 notifyAll() 메소드도 있는데, notify()는 wait()에 의해 일시 정지 된 스레드중 한개를 실행 대기 상태로 만들고, notifyAll() 메소드는 wait() 에 의해 일시 정지된 모든 스레드들을 실행 대기 상태로 만든다.
이 메소드들은 Thread 클래스가 아닌 Object 클래스에 선언된 메소드이므로 모든 공유 객체에서 호출이 가능하다
주의 할 점은 이메소드들은 동기화 메소드 또는 동기화 블록 내에서만 사용할 수 있다.
다음 예제는 두 스레드의 작업을 WorkObject의 methodA() 와 methodB() 에 정의해 두고,
두 스레드 ThreadA와 ThreadB 가 교대로 methodA() 와 methodB()를 호출 하도록 했다.
public class WorkObject {
public synchronized void methodA() {
System.out.println("ThreadA의 methodA() 작업 실행");
notify();// 일시 정지 상태에 있는 ThreadB를 실행 대기 상태로 만듬
try {
wait(); // Thread A를 일시 정지 상태로 만듬
} catch (InterruptedException e){
}
}
public synchronized void methodB() {
System.out.println("ThreadB의 methodB() 작업 실행");
notify();// 일시 정지 상태에 있는 ThredA를 실행 대기 상태로 만듬
try {
wait(); // Thread B를 일시 정지 상태로 만듬
} catch (InterruptedException e){
}
}
}
public class ThreadA extends Thread{
private WorkObject workObject;
public ThreadA(WorkObject workObject){
this.workObject = workObject; // 공유 객체를 매개값으로 받아 필드에 저장
}
@Override
public void run() {
for(int i=0; i< 10 ; i++){
workObject.methodA(); // 공유 객체의 methodA()를 10번 반복 호출
}
}
}
public class ThreadB extends Thread{
private WorkObject workObject;
public ThreadB(WorkObject workObject){
this.workObject = workObject; // 공유 객체를 매개값으로 받아 필드에 저장
}
@Override
public void run() {
for(int i=0; i< 10 ; i++){
workObject.methodA(); // 공유 객체의 methodB()를 10번 반복 호출
}
}
}
public class WaitNotifyExample {
public static void main(String[] args){
WorkObject shareObject = new WorkObject(); // 공유 객체 생성
//Thread A , B 객체 생성
ThreadA threadA = new ThreadA(shareObject);
ThreadB threadB = new ThreadB(shareObject);
// 쓰레드 실행
threadA.start();
threadB.start();
}
}
데이터를 저장하는 스레드(생산자 스레드) 가 데이터를 저장하면, 데이터를 소비하는 스레드(소비자 스레드) 데이터를 읽고 처리하는 교대 작업을 구현
생산자 스레드는 소비자 스레드가 읽기 전에 새로운 데이터를 두 번 생성하면 안 되고
소비자 스레드는 생산자 스레드가 새로운 데이터를 생성하기 전에 이전 데이터를 두번 읽어서도 안된다
구현 방법은 공유 객체에 데이터를 저장할 수 있는 data 필드의 값이 null 이면 생산자 스레드를 실행 대기 상태로 만들고
소비자 스레드를 일시 정지 상태로 만드는 것이다
반대로 data 필드의 값이 null이 아니면 소비자 스레드를 실행 대기 상태로 만들고,
생산자 스레드를 일시 정지 상태로 만들면 된다
public class DataBox{
private String data;
public syschronized 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 syschronized void setData(String data){
// data 필드가 null이 아니면 생산자 스레드를 일시 정지 상태로 만듬
if(this.data != null){
try {
wait();
} catch(InterruptedException e){
}
}
// data 필드에 값을 저장하고 소비자 스레드를 실행 대기 상태로 만듬
this.data = data;
System.out.pringln("ProducerThread가 생성한 데이터 : " + data);
notify();
}
}
public 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);// 새로운 데이터를 저장
}
}
}
public class ConsumerThread 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.getData(data);// 새로운 데이터를 읽음
}
}
}
public class WaitNotifyExample {
public static void maun(String[] args){
DataBox dataBox = new DataBox();
ProducerThread producerThread = new ProducerThread(dataBox);
ConsumerThread consumerThread = new ConsumerThread(dataBox);
producerThread.start();
consumerThread.start();
}
}
스레드의 안전한 종료 (Stop플래그 , interrupt())
스레드는 자신의 run 메소드가 모두 실행 되면 자동적으로 종료 된다.
경우에 따라서는 실행 중인 스레드를 즉시 종료할 필요가 있다.
에를 들어 동영상을 끝까지 보지 않고 사용자가 멈춤을 요구
Thread는 스레드를 즉시 종료 시키기 위해서 stop() 메소드를 제공하고 있는데, 이메소는 deprecated
그 이유는 stop() 메소드로 스레드를 갑자기 종료하게 되면 스레드가 사용 중이던 자원들이 불안정한 상태로 남겨짐
*여기서 자원이란 파일, 네트워크 연결등을 말함
스레드를 즉시 종료시키기 위헌 최선의 방법
stop 플래그를 이용하는 방법
스레드는 run() 메소드가 끝나면 자동적으로 종료되므로,
run() 메소드가 정상적으로 종료되도록 유도하는 것이 최선의 방법
다음 코드는 stop플래그를 이용해서 run() 메소드의 종료를 유도
public class XXXThread extends Thread{
private boolean stop; // stop 플래그 필드
public void run(){
while(!stop){ // stop이 true가 되면 run()이 종료된다.
스레드가 반복 실행하는 코드;
}
// 스레드가 사용한 자원 정리
}
}
위 코드에서 stop 필드가 false 일 경우에는 while문의 조건식이 true 가 되어 반복 실행하지만,
stop 필드가 true 일 경우에는 while문의 조건식이 false 가 되어 while문을 빠져나온다.
그리고 스레드가 사용한 자원을 정리하고, run() 메소드가 끝나게 됨으로써 스레드는 안전하게 종료된다.
다음 예제는 PrintThread1을 실행한 후 1초 후에 PrintThread1을 멈추도록 setStop() 메소드를 호출
public class StopFlagExample {
public static void main(String[] args){
PrintThread1 printThread = new PrintThread1();
printThread.start();
try{
Thread.sleep(1000);
} catch (InterruptedException e) {}
printThread.setStop(true); // 스레드를 종료시키기 위해 stop 필드를 true 로 변경
}
}
↑
public class PrintThread1 extends Thread{
private boolean stop;
public void setStop(boolean stop){
this.stop = stop;
}
public void run(){
while(!stop){
System.out.println("실행 중");
}
System.out.println("자원정리");
System.out.println("실행 종료");
}
}
interrupt() 메소드를 이용하는 방법
interrupt() 메소드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 한다.
이것을 이용하면 run() 메소드를 정상 종료 시 킬 수 있다.
다음 그림과 같이 ThreadA 가 ThreadB 를 생성해서 start() 메소드로 ThreadB를 실행했다고 가정
ThreadA가 ThreadB의 interrupt() 메소드를 실행 하게 되면 ThreadB가 sleep() 메소드로 일시 정지 상태가 될때
ThreadB에서 InterruptedException이 발생하여 예외 처리(catch) 블록으로 이동한다.
결국 ThreadB는 while문을 빠져나와 run() 메소드를 정상 종료 하게 된다.
다음 예제는 PrintThread2를 실행한 후 1초 후 PrintThread2를 멈추도록 interrupt() 메소드를 호출 한다
public class InterruptExample {
public static void main(){
Thread thread = new PrintThread2();
thread.start();
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
}
thread.interrupt(); // 스레드를 종료 시키기 위해 InterruptedException을 발생 시킴
}
}
public class PrintThread2 extends Thread {
public void run(){
try{
while(true) {
System.out.println("실행 중");
Thread.sleep(1);
}
} catch(InterruptException e){
}
}
System.out.println("자원 정리");
System.out.println("실행 종료 ");
}
** 스레드가 실행 대기 또는 실행 상태에 있을 때 interrupt() 메소드가 실행되면
즉시 InterruptedException 예외가 발생하지 않고, 스레드가 미래에 일시 정지 상태가 되면
InterruptedException 예외가 발생한다는 것이다.
따라서 스레드가 일시 정지 상태가 되자 않으면 interrupt() 메소드 호출은 아무런 의미가 없다.
그래서 짧은 시간이나마 일시 정지 시키기 위해서 Thread.sleep(1)을 사용한 것이다.
일시 정지를 만들지 않고도 interrupt() 호출 여부를 알 수 있는 방법이 있다.
interrupt() 메소드가 호출되었다면 스레드의 interrupted() 와 isInterrupted 메소드는 true를 리턴
interrupted() 는 정적 메소드로 현재 스레드가 interrupted() 되었는지 확인 하는 것이고,
isInterrupted() 는 인스턴스 메소드로 현재 스레드가 interrupted 되었는지 확인 할 때 사용한다.
둘 중 어떤 것을 사용해도 좋다.
boolean status = Thread.interrupted();
boolean status = objThread.isInterrupted();
다음은 PrintThread2를 수정한 것인데,
일시 정지 코드인 Thread.sleep(1)을 사용하지 않고,
Thread.interrupted()를 사용해서 PrintThread2의 interrupt()가 호출 되었는지 확인한 다음
while 문을 빠져나가도록 했따.
public class PrintThread2 extends Thread {
public void run(){
try{
while(true) {
System.out.println("실행 중");
if(Thread.interrupted()){
break;
}
}
} catch(InterruptException e){
}
}
System.out.println("자원 정리");
System.out.println("실행 종료 ");
}
'Back-end > 이것이 자바다[신용권 한빛미디어]' 카테고리의 다른 글
스레드 풀 (0) | 2021.12.12 |
---|---|
스레드 그룹(ThreadGroup) (0) | 2021.12.12 |
스레드 상태 (0) | 2021.12.04 |
동기화 메소드와 동기화 블록 (0) | 2021.12.04 |
스레드 우선순위 (0) | 2021.11.27 |