쓰레드는 양이 워낙 많고 중요해서 이렇게 나눠서 글을 작성하고 있습니다. 이번 글에서는 쓰레드를 통제하기
, 쓰레드를 그룹짓기
에 대해서 알아보겠습니다.
먼저 쓰레드를 통제하는 것에 대해서 알아보겠습니다.
두 개의 그림을 같이 참고하면 좋을 거 같습니다.
생성(new) : 프로세스가 메모리에 올라와 실행 준비를 완료한 상태입니다.
준비(ready) : 생성된 프로세스가 CPU를 얻을 때까지 기다리는 상태
수행(running) : 준비 상태에 있는 프로세스 중 하나가 CPU를 얻어 실제 작업을 수행하는 상태
대기(waiting) : 실행 상태에 있는 프로세스가 입출력을 요청하면 입출력이 완료될 때까지 기다리는 상태
종료(exit) : 프로세스가 종료된 상태
쓰레드는 이러한 상태 다이어그램을 가지고 있습니다. 그리고 자바에 Thread 클래스에 State라는 Enum 클래스를 가지고 있습니다.
public class Thread implements Runnable {
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
}
쓰레드는 이러한 상태를 가지면서 실행하게 됩니다.
interrupt() 메소드는 현재 수행중인 쓰레드를 중단시킵니다. 그냥 중단시키는 것은 아니고 InterruptedException
을 발생시키면서 중단시킵니다.
만약 쓰레드를 시작하기 전(NEW)이나, 쓰레드가 종료된 상태(TERMINATED)일 때, interrupt() 메소드를 호출하면 예외나 에러 없이 그냥 다음 문장으로 넘어갑니다.
이번에도 예제 코드를 보면서 이해해보겠습니다.
public class SleepThread extends Thread {
long sleepTime;
public SleepThread(long sleepTime) {
this.sleepTime = sleepTime;
}
@Override
public void run() {
try {
System.out.println("Sleeping " + getName());
Thread.sleep(sleepTime);
System.out.println("Stopping " + getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class RunSupportThreads {
public static void main(String[] args) {
RunSupportThreads sample = new RunSupportThreads();
sample.checkThreadState1();
}
public void checkThreadState1() {
SleepThread thread = new SleepThread(2000);
try {
System.out.println("thread state= " + thread.getState());
thread.start();
System.out.println("thread state(after start) = " + thread.getState());
Thread.sleep(1000);
System.out.println("thread state(after 1sec) = " + thread.getState());
thread.join();
thread.interrupt();
System.out.println("thread state(after join) = " + thread.getState());
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
}
thread state= NEW
thread state(after start) = RUNNABLE
Sleeping Thread-0
thread state(after 1sec) = TIMED_WAITING
Stopping Thread-0
thread state(after join) = TERMINATED
뭔가 코드가 복잡해보이지만 아래에 join()
메소드와 interrupt()
메소드에 대해서 알아보겠습니다.
join() : 해당 쓰레드가 종료될 때까지 기다리게 합니다.
SleepThread
를 2초간 재웠기 때문에 join() 메소드가 실행되고 있을 때는 아직 sleep() 상태일 것입니다. 그래서 위의 코드는 join()을 통해서 쓰레드가 깨어난 후 종료될 때까지
기다린 후에 interrupt() 메소드를 사용하면 이미 종료가 되었기 때문에 에러가 발생하지 않습니다.
그러면 join() 메소드를 빼면 어떻게 될까요?
thread state= NEW
thread state(after start) = RUNNABLE
Sleeping Thread-0
thread state(after 1sec) = TIMED_WAITING
thread state(after join) = TIMED_WAITING
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at Thread.SleepThread.run(SleepThread.java:14)
그러면 위와 같이 에러가 발생합니다. 위에서 말했듯이 쓰레드가 종료되지 않은 상태에서 interrupt() 메소드를 호출하면 위와 같이 에러가 발생하게 됩니다.
public class Object {
@HotSpotIntrinsicCandidate
public final native void notify();
@HotSpotIntrinsicCandidate
public final native void notifyAll();
public final void wait() throws InterruptedException {
wait(0L);
}
public final native void wait(long timeoutMillis) throws InterruptedException;
public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
if (timeoutMillis < 0) {
throw new IllegalArgumentException("timeoutMillis value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeoutMillis++;
}
wait(timeoutMillis);
}
}
Object 클래스 내부를 보면 위와 같이 쓰레드와 관련된 메소드들이 존재합니다.
wait() : 다른 쓰레드가 Object 객체에 대한 notify() 메소드나 notifyAll() 메소드를 호출할 때까지 현재 쓰레드가 대기하고 있도록 합니다.
notify() : Object 객체의 모니터에 대기하고 있는 단일 쓰레드를 깨웁니다.
notifyAll() : Object 객체의 모니터에 대기하고 있는 모든 쓰레드를 깨웁니다.
간단하게 메소드들에 대한 설명을 하면 위와 같습니다. 간단하게 말하면 wait 메소드를 사용하면 쓰레드가 대기 상태가 되고
, notify(), notifyAll()을 사용하면 쓰레드의 대기상태가 해제됩니다.
말로만 봐서는 감이 안오니 예제 코드를 보면서 알아보겠습니다.
public class StateThread extends Thread {
private Object monitor;
public StateThread(Object monitor) {
this.monitor = monitor;
}
@Override
public void run() {
try {
for (int loop = 0; loop < 10000; loop++) {
String a = "A";
}
synchronized (monitor) {
monitor.wait();
}
System.out.println(getName() + " is notified");
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
}
public class RunObjectThreads {
public static void main(String[] args) {
RunObjectThreads sample = new RunObjectThreads();
sample.checkThreadState2();
}
public void checkThreadState2() {
Object monitor = new Object();
StateThread thread = new StateThread(monitor);
try {
System.out.println("thread state = " + thread.getState());
thread.start();
System.out.println("thread state(after start) = " + thread.getState());
Thread.sleep(100);
System.out.println("thread state(after 0.1 sec) = " + thread.getState());
synchronized (monitor) {
monitor.notify();
}
Thread.sleep(100);
System.out.println("thread state(after notify) = " + thread.getState());
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
}
thread state = NEW
thread state(after start) = RUNNABLE
thread state(after 0.1 sec) = WAITING
Thread-0 is notified
thread state(after notify) = TERMINATED
위의 코드의 결과는 위와 같습니다. StateThread
에서 wait() 메소드를 통해서 waiting 시켰기 때문에 결과에서도 쓰레드가 WAITING 으로 바뀐 것을 볼 수 있습니다.
그리고 다시 notify()
메소드를 통해서 깨우니까 쓰레드가 실행상태로 돌아오고 종료가 된 것까지 확인할 수 있습니다.
그러면 만약 깨워야 할 쓰레드가 2개 이상이라면 어떻게 될까요? 아래의 예제코드를 보겠습니다.
public class RunObjectThreads {
public static void main(String[] args) {
RunObjectThreads sample = new RunObjectThreads();
sample.checkThreadState2();
}
public void checkThreadState2() {
Object monitor = new Object();
StateThread thread = new StateThread(monitor);
StateThread thread1 = new StateThread(monitor);
try {
System.out.println("thread state = " + thread.getState());
thread.start();
thread1.start();
System.out.println("thread state(after start) = " + thread.getState());
Thread.sleep(100);
System.out.println("thread state(after 0.1 sec) = " + thread.getState());
synchronized (monitor) {
monitor.notify();
}
Thread.sleep(100);
System.out.println("thread state(after notify) = " + thread.getState());
thread.join();
System.out.println("thread state(after join) = " + thread.getState());
thread1.join();
System.out.println("thread state(after join) = " + thread1.getState());
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
}
위의 예제 코드에서 살짝만 수정한 코드 입니다. 이 코드의 결과는 어떻게 나올까요? 위의 예제 코드는 쓰레드가 1개를 만들었는데 이번에는 2개를 만들었습니다.
두 개의 쓰레드 모두 wait()를 통해서 WAITING 상태로 갔는데 notify() 메소드 하나만 호출하면 둘 다 깨울 수 있을까요?
정답은 그럴 수 없습니다.
thread state = NEW
thread state(after start) = RUNNABLE
thread state(after 0.1 sec) = WAITING
Thread-1 is notified
thread state(after notify) = WAITING
결과는 이렇게 나오고 애플리케이션은 끝나지 않습니다. 왜 그럴까요? notify()
메소드는 먼저 대기하고 있는 쓰레드부터 그 상태를 풀어주기 때문에 나머지 쓰레드는 끝나지 않았던 것입니다.
쓰레드의 개수가 몇 개인지 알고 원하는 몇 개만 풀고 싶다면 아래와 같이 하는 방법도 있습니다.
synchronized (monitor) {
monitor.notify();
monitor.notify();
}
하지만 쓰레드가 몇 개가 wait() 하고 있는지 모른다면 notifyAll()
메소드를 사용하면 됩니다.
thread state = NEW
thread state(after start) = RUNNABLE
thread state(after 0.1 sec) = WAITING
Thread-0 is notified
Thread-1 is notified
thread state(after notify) = TERMINATED
thread state(after join) = TERMINATED
thread state(after join) = TERMINATED
그러면 위와 같이 결과가 나오면서 애플리케이션도 잘 끝나게 됩니다.
ThreadGroup은 쓰레드의 관리를 용이하게 하기 위한 클래스입니다. 하나의 애플리케이션에는 여러 종류의 쓰레드가 있기 때문에 용도에 맞는 쓰레드끼리 그룹을 지어서 관리하면 훨씬 편리합니다.
쓰레드 그룹은 보안상의 이유로 도입된 개념으로, 자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹은 변경할 수 있지만 다른 쓰레드 그룹의 쓰레드를 변경할 수는 없습니다.
쓰레드를 쓰레드 그룹에 포함시키려면 Thread의 생성자를 이용해야 합니다.
public Thread(Runnable target, String name)
public Thread(ThreadGroup group, Runnable target)
public Thread(ThreadGroup group, Runnable target, String name)
public Thread(ThreadGroup group, Runnable target, String name)
자바 애플리케이션이 실행되면, JVM은 main과 system이라는 쓰레드 그룹을 만들고 JVM 운영에 필요한 쓰레드들을 생성해서 이 쓰레드 그룹에 포함시킵니다.
예를들어, main 메소드를 수행하는 main이라는 이름의 쓰레드는 main쓰레드 그룹에 속하고, 가비지컬렉션을 수행하는 Finalizer 쓰레드는 system 쓰레드 그룹에 속합니다.
우리가 생성하는 모든 그룹은 main 쓰레드 그룹의 하위 쓰레드 그룹이 되며, 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 자동적으로 main 쓰레드 그룹에 속하게 됩니다.
public class Test {
public static void main(String[] args) {
ThreadGroup main = Thread.currentThread().getThreadGroup();
ThreadGroup grp1 = new ThreadGroup("Group1");
ThreadGroup grp2 = new ThreadGroup("Group2");
ThreadGroup subGrp1 = new ThreadGroup(grp1, "SubGroup1");
grp1.setMaxPriority(3);
Runnable r = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
new Thread(grp1, r, "th1").start();
new Thread(subGrp1, r, "th2").start();
new Thread(grp2, r, "th3").start();
System.out.println(">>List of ThreadGroup : " + main.getName() + ", Active ThreadGroup: " + main.activeGroupCount() + ", Active Thread: " + main.activeCount());
main.list();
}
}
>>List of ThreadGroup : main, Active ThreadGroup: 3, Active Thread: 5
java.lang.ThreadGroup[name=main,maxpri=10]
Thread[main,5,main]
Thread[Monitor Ctrl-Break,5,main]
java.lang.ThreadGroup[name=Group1,maxpri=3]
Thread[th1,3,Group1]
java.lang.ThreadGroup[name=SubGroup1,maxpri=3]
Thread[th2,3,SubGroup1]
java.lang.ThreadGroup[name=Group2,maxpri=10]
Thread[th3,5,Group2]
새로 생성한 모든 쓰레드 그룹은 main 쓰레드 그룹의 하위 쓰레드 그룹으로 포함되어 있다는 것도 볼 수 있습니다.