본글은 'Do it 자바 완전 정복' 책을 통한 공부내용을 정리한 글입니다.
쓰레드는 객체가 생성, 실행, 종료되기까지 다양한 상태를 가지며 각 쓰레드의 상태는 Thread.State 타입으로 정의되어 있다. Thread의 인스턴스 메서드 getState()를 이용해 쓰레드의 상태를 Thread.State 타입에 저장된 문자열 상숫값 중 하나로 리턴한다.
Thread.State getState();
Thread.State는 enum 타입이다. 내부에 6개의 문자열 상수가 저장되어 있다. 쓰레드의 상태에 따라 특정 작업 수행시에는 아래와 코드와 같이 switch 선택 제어문을 이용한다.
Thread.State state = myThread.getState();
switch (state) {
case Thread.State.NEW:
// ..
case Thread.State.RUNNABLE:
// ..
case Thread.State.TERMINATED:
// ..
case Thread.State.TIMED_WAITING:
// ..
case Thread.State.BLOCKED:
// ..
case Thread.State.WAITING:
// ..
}
쓰레드의 6가지 상태
NEW, RUNNABLE, TERMINATED
처음 객체가 생성되면 NEW의 상태를 가지며 이후 start() 메서드 실행시 RUNNABLE 상태가 된다. 이후 run() 메서드 종료시 TERMINATED 상태가 된다. RUNNABLE 상태에서는 상황에 따라 TIMED_WAITING, BLOCKED, WAITING 3가지의 일시정지 상태로 전환될 수 있다.
TIMED_WAITING
정적 메서드인 Thread.sleep(), 인스턴스 메서드 join()
이 호출되면 쓰레드는 TIMED_WAITING 상태가 된다. 일정 시간동안 일시정지 되는것이며 설정한 일시정지 시간이 지나거나 interrup()
메서드를 중간에 호출시 RUNNALBE 상태가 된다.
두개의 메서드의 차이는 예를들어 쓰레드 A에서 Thread.sleep(1000)
실행시 쓰레드 A는 외부에서 interrupt() 메서드가 호출되지 않는 한 1초동안 일시정지 상태가 유지될 것이다. 쓰레드B객체.join(1000)
과 같이 호출시에는 나는 일시정지하고 쓰레드 B에게 CPU를 할당하라는 의미이므로 B가 0.5초만에 실행된다면 외부 interrupt()
메서드의 호출 없이도 쓰레드는 다시 RUNNABLE 상태가 된다.
BLOCKED
동기화 메서드 또는 동기화 블록을 실행하기 위해 먼저 실행 중인 쓰레드의 실행 완료를 기다리는 상태. 앞의 쓰레드의 동기화 영역 수행이 완료되면 RUNNABLE 상태가 되어 해당 동기화 영역을 실행하게 된다.
WAITING
시간 정보가 없는 join()
메서드가 호출되거나 wait()
메서드가 호출된다면 WAITING 상태로 변경된다.
// RUNNABLE -> WAITING 전환 방법 1
synchronized void join()
// RUNNABLE -> WAITING 전환 방법 2
void wait()
WAITING상태에서 RUNNABLE상태로 돌아가는 방법은 어떤 메서드를 이용해 WAITING 상태가 되었는지에 따라 달라진다. 1번 방법으로 WAITING 상태가 되었다면, join()
의 대상이 된 쓰레드가 종료되거나 외부에서 interrupt()
메서드가 호출되면 RUNNABLE 상태로 돌아간다. 2번방법으로 WAITING 상태가 되었다면, Object 클래스의 notify() 또는 notifyAll()
메서드를 이용해 돌아갈 수 있다. 주의해야할 점은 wait(), notify(), notifyAll() 메서드들은 동기화 블록 내에서만 사용할 수 있다.
NEW, RUNNABLE, TERMINATED
이제 각 쓰레드 상태를 예제 코드와 함께 자세히 알아본다.
NEW 상태는 Thread 객체를 new 키워드를 이용해 생성한 시점으로 아직 start() 메서드 호출 이전 상태, 즉 실행 이전 상태다. 이후 start() 메서드를 호출하면 RUNNABLE 상태가 되며 이후 run() 메서드가 완전히 종료되면 쓰레드는 TERMINATED 상태가 된다고 위에서 언급했다.
public class NewRunnalbeTerminated {
public static void main(String[] args) {
Thread.State state;
Thread myThread = new Thread() {
@Override
public void run() {
for(long i = 0; i < 100000000L ; i++) {}
// 시간지연
}
};
state = myTread.getState();
System.out.println(state); // NEW
myThead.start(); // 쓰레드 시작
state = myTread.getState();
System.out.println(state); // RUNNABLE
try {
myThread.join();
// myThread 실행이 완료될 때 까지 main 쓰레드 일시 정지
} catch (InterruptedException e) {}
state = myTread.getState();
System.out.println(state); // TERMINATED
}
}
RUNNABLE 상태에서는 쓰레드 간의 동시성에 따라 실행과 실행 대기를 반복한다. Thread의 정적 메서드 yield()를 사용하면 다른 쓰레드에게 CPU 사용을 인위적으로 양보하고, 자신은 실행 대기 상태로 전환할 수 있다.
static void Thread.yield();
영영 CPU 사용을 양보하는 것은 아니고, 자신의 차례를 딱 한번 양보한다.
class MyThread extends Thread {
boolean yieldFlag;
@Override
public void run() {
while(true) {
if(yieldFlag) { Thread.yield(); }
// yieldFlag가 true라면 다른 쓰레드에게 CPU 사용권 양보
else {
System.out.println(getName());
for(long i = 0; i < 100000000L ; i++) {}
}
}
}
}
public class YieldState {
public static void main(String args[]) {
MyThread thread1 = new MyThread();
thread1.setName("thread1");
thread1.yieldFlag = false;
thread1.setDaemon(true);
thread1.start();
MyThread thread2 = new MyThread();
thread2.setName("thread2");
thread2.yieldFlag = true;
thread2.setDaemon(true);
thread2.start();
// 1초마다 한번씩 양보
for(int i = 0; i < 6 ; i++) {
try {Thread.sleep(1000);} catch(InterruptedException e) {}
thread1.yieldFlag = !thread1.yieldFlag;
thread2.yieldFlag = !thread2.yieldFlag;
}
}
}
TIMED_WAITING
RUNNABLE 상태에서 TIMED_WAITING 상태가 됐을 때에는 Thread의 정적 메서드인 sleep(long millis)
를 호출하거나, 인스턴스 메서드인 join(long millis)
가 호출됐을 때다. 둘은 정적메서드, 인스턴스 메서드라는 점 이외에 메서드의 의미 또한 다르므로 명확히 구분할 필요가 있다.
Thread.sleep()
은 이 메서드를 호출한 쓰레드를 일시정지하라는 의미다.쓰레드객체.join()
메서드는 특정 쓰레드 객체에게 일정 시간 CPU를 할당하라는 의미로, 일반적으로 다른 쓰레드의 결과가 먼저 나와야 이후의 작업을 진행할 때 사용할 수 있다. 이 메서드를 호출한 쓰레드도 sleep()
메서드와 마찬가지로 TIMED_WAITING 상태가 된다.
결론적으로,메서드 두개 모두 호출한 쓰레드는 일시정지 되는것이다.
예시 코드
class MyThread extends Thread {
@Override
public void run() {
try{
Thread.sleep(3000);
} catch(IntteruptedException e) {
System.out.println("sleep 진행중 innterrupt() 발생")
for(long i = 0; i < 100000000L ; i++) {} // 시간 지연
}
}
}
public class TimeWaiting {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
try {Thread.sleep(100);} catch(InterruptedException e) {}
System.out.println(myThread.getState()); // TIME_WAITING
myThread.interrupt(); // TIME_WAITING -> RUNNABLE 상태 전환
try {Thread.sleep(100);} catch(InterruptedException e) {}
System.out.println(myThread.getState()); // RUNNABLE
}
}
Thread.sleep(100)
을 실행한 이유는 자바 가상 머신이 준비 과정을 미리 거쳐야 하므로 시간 지연을 지켜준 것이다.
이제 join() 메서드 호출을 통해 RUNNABLE 상태에서 TIME_WAITING 상태가 됐을때를 살펴본다.
class MyThread1 extends Thread {
@Override
public void run() {
for(long i = 0; i < 100000000L ; i++) {} // 시간 지연
}
}
class MyThread2 extends Thread {
MyThread1 myThread1;
public MyThread2(MyThread1 myThread1) {
this.myThread1 = myThread1;
}
@Override
public void run() {
try{
myThread1.join(3000);
// myThread1에게 최대 3초 동안 CPU 우선 사용권 부여
} catch(IntteruptedException e) {
System.out.println("join 진행중 innterrupt() 발생")
for(long i = 0; i < 100000000L ; i++) {} // 시간 지연
}
}
}
public class TimeWaiting_join {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
MyThread2 myThread2 = new MyThread2(myThread1);
myThread1.start();
myThread2.start();
try {Thread.sleep(100);} catch(InterruptedException e) {}
System.out.println(myThread1.getState()); // RUNNABLE
System.out.println(myThread2.getState()); // TIME_WAITING
myThread2.interrupt(); // TIME_WAITING -> RUNNABLE 상태 전환
try {Thread.sleep(100);} catch(InterruptedException e) {}
System.out.println(myThread1.getState()); // RUNNABLE
System.out.println(myThread2.getState()); // RUNNABLE
}
}
BLOCKED
BLOCKED는 동기화 메서드 또는 동기화 블록을 실행하고자 할 때 이미 다른 쓰레드가 해당 영역을 실행하고 있는 경우 발생한다.
예시 코드
class MyBlockTest {
MyClass mc = new MyClass();
Thread t1 = new Thread("thread1") {
public void run() {
mc.syncMethod();
};
};
Thread t2 = new Thread("thread2") {
public void run() {
mc.syncMethod();
};
};
Thread t3 = new Thread("thread3") {
public void run() {
mc.syncMethod();
};
};
void StartAll() {
t1.start(); t2.start(); t3.start();
}
class MyClass {
synchronized void syncMethod() {
try {Thread.sleep(100);} catch(InterruptedException e) {}
System.out.println(Thread.currentThread().getName());
System.out.println(t1.getState());
System.out.println(t2.getState());
System.out.println(t3.getState());
for(long i = 0 ; i < 100000000L ; i++) {}
}
}
}
public class BlockedState {
public static void main(String args[]) {
MyBlockTest mbt = new MyBlockTest();
mbt.startAll();
}
}
동기화 영역에 실행 명령이 도착한 순서가 t1, t2, t3순이라면 가장 먼저 도착한 t1쓰레드가 동기화 영역을 실행하고, t2, t3 쓰레드는 BLOCKED 상태가 될 것이다. t1 쓰레드는 실행을 완료한 후 동기화 영역에 열쇠를 반납하므로 t2, t3가 다시 경쟁하여 먼저 도착하는 쓰레드가 실행 권한을 갖게 된다.
WAITING
일시정지하는 시간의 지정 없이 쓰레드객체.join()
메서드 호출시 조인된 쓰레드 객체의 실행이 완료될 때 까지 이를 호출한 쓰레드는 WAITING 상태가 된다. 이때 조인된 쓰레드가 완료되거나 interrupt()
메서드 호출로 예외를 발생시켰을 때만 다시 RUNNABLE 상태로 돌아갈 수 있다.
또는 wait()
메서드를 호출할 때도 WAITING 상태가 된다. 주의해야할 점은 wait() 메서드로 WAITING 상태가 된 쓰레드는 다른 쓰레드에서 notify()
또는 notifyAll()
을 호출해야만 RUNNABLE 상태가 될 수 있다. (=스스로 WAITING 상태를 벗어날 수 없다.) 다른 쓰레드에서 해당 메서드가 실행되면 일시 정지됐던 지점인 wait()
의 다음줄 부터 실행되므로 신경써서 작성한다. 그리고 wait(),notify(),notifyAll()
메서드는 반드시 동기화 블록에서만 사용할 수 있다.
예시 코드
class DataBox {
boolean isEmpty = true; // 데이터가 비어있는지 검사하는 boolean 변수
int data;
sychronized void inputData(int data) {
if(!isEmpty) {
try { wait(); } catch (InterruptedException e) {}
}
this.data = data;
isEmpty = false;
System.out.println(data);
notify();
}
sychronized void outputData() {
if(isEmpty) {
try { wait(); } catch (InterruptedException e) {}
}
isEmpty = true;
System.out.println(data);
notify();
}
}
public class Wait_Notify {
public static void main(String[] args) {
DataBox dataBox = new DataBox();
Thread t1 = new Thread() {
public void run() {
for(int i = 1; i < 9 ; i++) {
dataBox.inputData(i);
}
}
}
Thread t2 = new Thread() {
public void run() {
for(int i = 1; i < 9 ; i++) {
dataBox.outputData();
}
}
}
t1.start(); t2.start();
}
}
동작 순서
1) 쓰기 쓰레드 동작(데이터 쓰기)
2) 읽기 쓰레드 깨우기 notify()
3) 쓰기 쓰레드 정지 wait()
4) 읽기 쓰레드 동작(데이터 읽기)
5) 쓰기 쓰레드 깨우기 notify()
6) 읽기 쓰레드 일시정지 wait()
... 반복
'Backend > JAVA' 카테고리의 다른 글
[JAVA] #17 컬렉션 프레임워크, List<E> 컬렉션 인터페이스 (1) | 2023.07.18 |
---|---|
[JAVA] #16 제네릭, 제네릭 클래스와 인터페이스, 제네릭 메서드 (1) | 2023.07.16 |
[Java] #15.4 쓰레드의 동기화 (0) | 2023.07.08 |
[Java] #15.3 쓰레드의 속성 (0) | 2023.07.05 |
[JAVA] #15.1, 15.2 쓰레드, 쓰레드의 생성 및 실행 (0) | 2023.07.04 |