스레드 이름
스레드는 자신의 이름을 가지고 있다. 메인 스레드는 'main'이라는 이름을 가지고 있고, 작업 스레드는 자동적으로 'Thread-n'이라는 이름을 가진다. 작업 스레드의 이름을 Thread-n 대신 다른 이름으로 설정하고 싶다면 Thread 클래스의 setName() 메서드를 사용하면 된다.
thread.setName("스레드 이름");
스레드 이름은 디버깅할 때 어떤 스레드가 작업을 하는지 조사할 목적으로 주로 사용된다. 현재 코드를 어떤 스레드가 실행하고 있는지 확인하려면 정적 메서드인 currentThread()로 스레드 객체의 참조를 얻은 다음 getName() 메서드로 이름을 출력해 보면 된다.
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
예제 코드
package ch14;
public class ThreadNameExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
Thread mainThread = Thread.currentThread();
System.out.println(mainThread.getName() + " 실행");
for(int i=0; i<3; i++) {
Thread threadA = new Thread() {
@Override
public void run() {
System.out.println(getName() + " 실행");
}
};
threadA.start();
}
Thread chatThread = new Thread() {
@Override
public void run() {
System.out.println(getName() + " 실행");
}
};
chatThread.start();
}
}
출력 결과
main 실행
Thread-0 실행
Thread-1 실행
Thread-2 실행
Thread-3 실행
스레드 상태
스레드 객체를 생성하고, star() 메서드를 호출하면 곧바로 스레드가 실행되는 것이 아니라 실행 대기 상태(Runnable)이 된다. 실행 대기 상태의 스레드는 CPU 스케쥴링에 따라 CPU를 점유하고 run() 메서드를 실행한다. 이때를 실행(Running) 상태라고 한다. 실행 스레드는 run() 메서드를 모두 실행하기 전에 스케쥴링에 의해 다시 실행 대기 상태로 돌아갈 수 있다. 그리고 다른 스레드가 실행 상태가 된다.
이렇게 스레드는 실행 대기 상태와 실행 상태를 번갈아 가면서 자신의 run() 메서드를 조금씩 실행한다. 실행 상태에서 run() 메서드가 종료되면 더 이상 실행할 코드가 없기 때문에 스레드의 실행은 멈추게 된다. 이 상태를 종료 상태(Terminated)라고 한다.
실행 상태에서 일시 정지 상태로 가기도 하는데, 일시 정지 상태는 스레드가 실행할 수 없는 상태를 말한다. 스레드가 다시 실행 상태로 가기 위해서는 일시 정지 상태에서 실행 대기 상태로 가야만 한다. 다음은 일시 정지로 가기 위한 메서드와 벗어나기 위한 메서드이다.
| 구분 | 메서드 | 설명 |
| 일시 정지로 보냄 | sleep(long millis) | 주어진 시간 동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. |
| join() | join() 메서드를 호출한 스레드는 일시 정지 상태가 된다. 실행 대기 상태가 되려면, join() 메서드를 가진 스레드가 종료되어야 한다. | |
| wait() | 동기화 블록 내에서 스레드를 일시 정지 상태로 만든다. | |
| 일시 정지에서 벗어남 | interrupt() | 일시 정지 상태일 경우, InterruptedException을 발생시켜 실행 대기 상태 또는 종료 상태로 만든다. |
| notify() notifyAll() |
wait() 메서드로 인해 일시 정지 상태인 스레드를 실행 대기 상태로 만든다. | |
| 실행 대기로 보냄 | yield() | 실행 상태에서 다른 스레드에게 실행을 양보하고 실행 대기 상태가 된다. |
주어진 시간 동안 일시 정지
실행중인 스레드를 일정 시간 멈추게 하고 싶다면 Thread 클래스의 정적 메서드인 sleep()을 이용하면 된다. 매개값에는 얼마 동안 일시 정지 상태로 있을 것인지 밀리세컨드(1/1000) 단위로 시간을 주면 된다.
예제 코드
package ch14;
import java.awt.Toolkit;
public class SleepExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i=0; i<10; i++) {
toolkit.beep();
try {
Thread.sleep(3000);
} catch(InterruptedException e) {
}
}
}
}
다른 스레드의 종료를 기다림
스레드는 다른 스레드와 독립적으로 실행하지만 다른 스레드가 종료될 때까지 기다렸다가 실행을 해야 하는 경우도 있다. 예를 들어 계산 스레드의 작업이 종료된 후 그 결괏값을 받아 처리하는 경우이다. 이를 위해 스레드는 join() 메서드를 제공한다.
예제 코드
package ch14;
public class SumThread extends Thread {
private long sum;
public long getSum() {
return sum;
}
public void setSum(long sum) {
this.sum = sum;
}
@Override
public void run() {
for(int i=1; i<=100; i++) {
sum+=i;
}
}
}
package ch14;
public class JoinExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
SumThread sumThread = new SumThread();
sumThread.start();
try {
sumThread.join();
} catch (InterruptedException e) {
}
System.out.println("1~100 합: " + sumThread.getSum());
}
}
출력 결과
1~100 합: 5050
다른 스레드에게 실행 양보
스레드가 처리하는 작업은 반복적인 실행을 위해 for 문이나 while 문을 포함하는 경우가 많은데, 가끔 반복문이 무의미한 반복을 하는 경우가 있다. 다음 코드를 보자. work의 값이 false라면 wile 문은 어떠한 실행문도 실행하지 않고 무의미한 반복을 한다.
public void run() {
while(true) {
if(work) {
System.out.println("ThreadA 작업 내용");
}
}
}
이때는 다른 스레드에게 실행을 양보하고 자신은 실행 대기 상태로 가는 것이 프로그램 성능에 도움이 된다. 이런 기능을 위해 Thread는 yield() 메서드를 제공한다. yield()를 호출한 스레드는 실행 대기 상태로 돌아가고, 다른 스레드가 실행 상태가 된다. 다음은 무의미한 반복을 하지 않고 다른 스레드에게 실행을 양보하도록 이전 코드를 수정한 것이다.
public void run() {
while(true) {
if(work) {
System.out.println("ThreadA 작업 내용");
} else {
Thread.yield();
}
}
}
다음 예제는 처음 5초 동안은 ThreadA와 ThreadB가 번갈아가며 실행하다가 5초 뒤에 메인 스레드가 ThreadA의 work 필드를 false로 변경함으로써 ThreadA가 yield() 메서드를 호출한다. 따라서 ThreadB가 더 많은 실행 기회를 얻게 된다. 그리고 10초 뒤에 ThreadA의 work 필드를 true로 변경해 다시 ThreadA와 ThreadB가 번갈아 가며 실행되도록 한다.
package ch14;
public class WorkThread extends Thread {
public boolean work = true;
public WorkThread(String name) {
setName(name);
}
@Override
public void run() {
while(true) {
if(work) {
System.out.println(getName() + ": 작업처리");
} else {
Thread.yield();
}
}
}
}
package ch14;
public class YieldExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
WorkThread workThreadA = new WorkThread("workThreadA");
WorkThread workThreadB = new WorkThread("workThreadB");
workThreadA.start();
workThreadB.start();
try { Thread.sleep(5000); } catch(InterruptedException e) {}
workThreadA.work = false;
try { Thread.sleep(10000); } catch(InterruptedException e) {}
workThreadA.work = true;
}
}
스레드 동기화
멀티 스레드는 하나의 객체를 공유해서 작업할 수도 있다. 이 경우, 다른 스레드에 의해 객체 내부 데이터가 쉽게 변경될 수 있기 때문에 의도했던 것과는 다른 결과가 나올 수 있다. 이를 위해 자바는 동기화 메서드와 블록을 제공한다. 객체 내부에 동기화 메서드와 동기화 블록이 여러 개가 있다면 스레드가 이들 중 하나를 실행할 때 다른 스레드는 해당 메서드는 물론이고 다른 동기화 메서드 및 블록도 실행할 수 없다. 하지만 일반 메서드는 실행이 가능하다.
동기화 메서드 및 블록 선언
동기화 메서드를 선언하는 방법은 다음과 같이 synchronized 키워드를 붙이면 된다. 해당 키워드는 인스턴스와 정적 메서드 어디든 붙일 수 있다.
public synchronized void nethod() {
//단 하나의 스레드만 실행하는 영역
}
스레드가 동기화 메서드를 실행하는 즉시 객체는 잠금이 일어나고, 메서드 실행이 끝나면 잠금이 풀린다. 메서드 전체가 아닌 일부 영역을 실행할 때만 객체 잠금을 걸고 싶다면 다음과 같이 동기화 블록을 만들면 된다.
public void method() {
//여러 스레드가 실행할 수 있는 영역
synchronized(공유객체) {
}
}
다음 예제는 공유 객체로 사용할 Calculator이다. setMemory1()을 동기화 메서드로, setMemory2()를 동기화 블록을 포함하는 메서드로 선언했다. 따라서 setMemory1과 setMemory2는 하나의 스레드만 실행 가능한 메서드가 된다.
package ch14;
public class Calculator {
private int memory;
public int getMemory() {
return memory;
}
// 동기화 메서드
public synchronized void setMemory1(int memory) {
this.memory = memory;
try {
Thread.sleep(2000);
} catch(InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}
// 동기화 블록
public void setMemory2(int memory) {
synchronized(this) {
this.memory = memory;
try {
Thread.sleep(2000);
} catch(InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}
}
}
다음 예제는 Calculator를 공유해서 사용하는 User1Thread와 User2Thread를 보여 준다.
package ch14;
public class User1Thread extends Thread {
private Calculator calculator;
public User1Thread() {
setName("User1Thread");
}
public void setCalculator(Calculator calculator) {
this.calculator = calculator;
}
@Override
public void run() {
calculator.setMemory1(100);
}
}
package ch14;
public class User2Thread extends Thread {
private Calculator calculator;
public User2Thread() {
setName("User2Thread");
}
public void setCalculator(Calculator calculator) {
this.calculator = calculator;
}
@Override
public void run() {
calculator.setMemory2(50);
}
}
다음 예제는 Calculator를 생성해서 User1Thread와 User2Thread에서 사용하도록 setCalculator() 메서드를 호출하고, 두 스레드를 실행시킨다.
package ch14;
public class SynchronizedExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
Calculator calculator = new Calculator();
User1Thread user1Thread = new User1Thread();
user1Thread.setCalculator(calculator);
user1Thread.start();
User2Thread user2Thread = new User2Thread();
user2Thread.setCalculator(calculator);
user2Thread.start();
}
}
정확히 User1Thread가 저장한 값이 100이 출력이 되었고, User2Thread가 저장한 값이 50이 출력되었다. User1Thread는 동기화 메서드인 setMemory1()을 실행하는 순간 Calculator의 객체를 잠근다. 따라서 User2Thread는 객체가 잠금 해제될 때까지 Calculator의 동기화 블록을 실행하지 못한다. 2초 일시 정지 후에 잠금이 해제되면 비로소 User2Thread가 동기화 블록을 실행한다.
wait()과 notify()를 이용한 스레드 제어
경우에 따라서는 두 개의 스레드를 교대로 번갈아 가며 실행할 때도 있다. 정확한 교대 작업이 필요할 경우, 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고 자신은 일시 정지 상태로 만들면 된다. 이 방법의 핵심은 공유 객체에 있다. 공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메서드로 정해 놓는다. 한 스레드가 작업을 완료하면 notify() 메서드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고, 자신은 두 번 작업을 하지 않도록 wait() 메서드를 호출해 일시 정지 상태로 만든다.
notify()는 wait()에 의해 일시 정지된 스레드 중 한 개를 실행 대기 상태로 만들고, notifyAll()은 모든 스레드를 실행 대기 상태로 만든다. 주의할 점은 이 두 메서드가 동기화 메서드 또는 동기화 블록 내에서만 사용할 수 있다는 것이다.
다음 예제는 WorkObject에 두 스레드가 해야 할 작업을 동기화 메서드인 methodA()와 methodB()로 각각 정의해 두고, ThreadA와 ThreadB가 교대로 메서드를 호출하도록 한 것이다.
package ch14;
public class WorkObject {
public synchronized void methodA() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + ":methodA 작업 실행");
notify();
try { wait(); } catch(InterruptedException e) {}
}
public synchronized void methodB() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + ":methodB 작업 실행");
notify();
try { wait(); } catch(InterruptedException e) {}
}
}
package ch14;
public class ThreadA extends Thread {
private WorkObject workObject;
public ThreadA(WorkObject workObject) {
setName("ThreadA");
this.workObject = workObject;
}
@Override
public void run() {
for(int i=0; i<10; i++) {
workObject.methodA();
}
}
}
package ch14;
public class ThreadB extends Thread {
private WorkObject workObject;
public ThreadB(WorkObject workObject) {
setName("ThreadB");
this.workObject = workObject;
}
@Override
public void run() {
for(int i=0; i<10; i++) {
workObject.methodB();
}
}
}
package ch14;
public class WaitNotifyExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
WorkObject workObject = new WorkObject();
ThreadA threadA = new ThreadA(workObject);
ThreadB threadB = new ThreadB(workObject);
threadA.start();
threadB.start();
}
}
스레드 안전 종료
스레드는 자신의 run() 메서드가 모두 실행되면 자동적으로 종료되지만, 경우에 따라서는 실행 중인 스레드를 즉시 종료할 필요가 있다. 예를 들면 동영상을 끝까지 보지 않고 사용자가 멈춤을 요구하는 경우이다. 스레드를 강제 종료하기 위해 Thread는 stop() 메서드를 제공하고 있으며 이 메서드는 deprecated(더 이상 사용하지 않음) 되었다. 왜냐하면 스레드를 갑자기 종료하게 되면 사용 중이던 리소스들이 불안전한 상태로 남겨지기 때문이다. 여기서 리소스란 파일, 네트워크 연결 등을 말한다. 스레드를 안전하게 종료하는 방법은 사용하던 리소스들을 정리하고 run() 메서드를 빨리 종료하는 것이다. 주로 조건 이용 방법과 interrupt() 메서드 이용 방법을 사용한다.
조건 이용
스레드가 while 문으로 반복 실행할 경우, 조건을 이용해서 run() 메서드의 종료를 유도할 수 있다.
다음 코드는 stop 필드 조건에 따라서 run() 메서드의 종료를 유도한다.
package ch14;
public class PrintThread extends Thread {
private boolean stop;
// 외부에서 stop 값을 수정할 수 있도록 Setter 선언
public void setStop(boolean stop) {
this.stop = stop;
}
@Override
public void run() {
// stop 필드값에 따라 반복 여부 결정
while(!stop) {
System.out.println("실행 중");
}
System.out.println("리소스 정리");
System.out.println("실행 종료");
}
}
package ch14;
public class SafeStopExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
PrintThread printThread = new PrintThread();
printThread.start();
try {
Thread.sleep(3000);
} catch(InterruptedException e) {
}
printThread.setStop(true);
}
}
interrupt() 메서드 이용
interrupt() 메서드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 한다. 이것을 이용하면 예외 처리를 통해 run() 메서드를 정상 졸료시킬 수 있다.
다음은 이전 예제에서 stop 필드 대신에 interrupt() 메서드를 이용해서 PrintThread를 종료하도록 수정한 것이다.
package ch14;
public class PrintThread extends Thread {
public void run() {
try {
while(true) {
System.out.println("실행 중");
Thread.sleep(1); //일시 정지를 만듦(예외가 발생하도록)
}
} catch(InterruptedException e) {
System.out.println("리소스 정리");
System.out.println("실행 종료");
}
}
}
package ch14;
public class SafeStopExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
PrintThread printThread = new PrintThread();
printThread.start();
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
}
printThread.interrupt();
}
}
스레드가 실행 대기/실행 상태일 때는 interrupt() 메서드가 호출되어도 예외가 발생하지 않는다. 그러나 스레드가 어떤 이유로 일시 정지 상태가 되면, 예외가 발생한다. 그래서 짧은 시간이나마 일시 정지를 위해 Thread.sleep(1)을 사용한 것이다.
일시 정지를 만들지 않고도 interrupt() 메서드 호출 여부를 알 수 있는 방법이 있다. Thread의 interrupted()와 isInterrupted() 메서드를 사용하는 것이다. 이 메서드를 사용하면 interrupt() 메서드 호출 여부를 리턴한다. 차이점은 전자는 정적 메서드이고, 후자는 인스턴스 메서드이다.
boolean status = Thread.interrupted();
boolean status = objThread.isInterrupted();
다음은 이전 에제를 수정해서 interrupted() 메서드로 interrupt() 메서드가 호출되었는지 확인한 다음 while 문을 빠져나가도록 한 것이다.
package ch14;
public class PrintThread extends Thread {
public void run() {
while(true) {
System.out.println("실행 중");
if(Thread.interrupted()) {
break;
}
}
System.out.println("리소스 정리");
System.out.println("실행 종료");
}
}
package ch14;
public class SafeStopExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
PrintThread printThread = new PrintThread();
printThread.start();
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
}
printThread.interrupt();
}
}
'Language > JAVA' 카테고리의 다른 글
| [JAVA] List 컬렉션 (0) | 2024.10.17 |
|---|---|
| [JAVA] 데몬 스레드와 스레드풀 (3) | 2024.10.17 |
| [JAVA] 멀티 스레드 (2) | 2024.10.17 |
| [JAVA] 타입 파라미터 (0) | 2024.10.16 |
| [JAVA] 제네릭 메서드 (0) | 2024.10.16 |