멀티스레드는 동시성 또는 병렬성으로 실행되기 때문에 정확히 이해하는 것이 좋습니다. 동시성은 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행하는 성질입니다. 병렬성은 멀티 작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 성질을 의미합니다. 스레드의 개수가 코어 수보다 많을 경우 스레드를 어떤 순서에 의해 동시성으로 실행할 것인가를 결정해야 하는데 이것을 스레드 스케줄링 이라고합니다.
자바의 스레드 스케줄링은 우선순위 방식과 순환 할당 방식을 사용합니다. 우선순위 방식은 우선순위가 높은 스레드가 실행 상태를 더 많이 갖도록 하는 스케줄링을 의미합니다. 순환 할당 방식은 시간 할당량을 정해 하나의 스레드를 정해진 시간만큼 실행하고 다시 다른 스레드를 실행하는 방식입니다. 우선순위 방식은 스레드 객체에 우선순위 번호를 개발자가 부여할 수 있습니다. 순환 할당 방식은 JVM 기계에 의해 정해지기 때문에 코드로 제어할 수 없습니다.
thread.setPriority(우선순위);
thread.setPriority(Thread.MAX_PRIORITY);
thread.setPriority(Thread.NORM_PRIORITY);
MAX_PRIORITY는 10, NORM_PRIORITZ는 5.. 각각 값을 갖고 있습니다.
싱글 스레드 프로그램에서 한 개의 스레드가 객체를 독차지해서 사용하면 되지만 멀티 스레드 프로그램에서는 스레드들이 객체를 공유해서 작업하는 경우가 있습니다. 스레드가 사용중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸어 다른 스레드가 사용할 수 없도록 해야 합니다. 임계 영역(critical section)이라고 하며, 자바는 임계 영역을 지정하기 위해 동기화(synchronoized) 메소드와 동기화 블록을 제공하고 있습니다. 스레드가 객체 내부의 동기화 메서드 또는 블록에 들어가면 즉시 잠금을 걸어 스레드가 임계 영역 코드를 실행하지 못하도록 합니다. 메서드 선언에 synchronized 키워드를 붙이면 됩니다.
public void method() {
// 여러 스레드가 실행 가능한 영역
...
synchronized(공유 객체) { // -> 동기화 블록, 공유 객체가 자기 자신이면 this를 넣을 수 있습니다.
// 임계영역
}
// 여러 스레드가 실행 가능한 영역
}
동기화 블록은 모두 실행할 때까지 다른 스레드들은 this 의 모든 동기화 메서드 또는 동기화 블록을 진행할 수 없게 됩니다.
스레드 객체를 생성하고 start() 메서드를 호출하면 곧바로 스레드가 실행되는 것처럼 보이지만 사실은 실행 대기 상태가 됩니다. 실행 대기 상태란 아직 스케줄링이 되지 않아 실행을 기다리고 있는 상태입니다. 실행 대기 상태에 있는 스레드 중에서 스레드 스케줄링으로 선택된 스레드가 바로 CPU를 점유하고 run() 메서드를 실행합니다. 이때 실행(Running) 상태라고 합니다. 실행 상태의 스레드는 run() 메서드를 모두 실행하기 전에 스레드 스케줄링에 의해 다시 실행 대기 상태로 돌아 갈 수 있습니다. 그리고 실행 대기 상태에 있는 다른 스레드가 선택되어 실행 상태가 됩니다. 이렇게 스레드는 실행 대기 상태와 실행 상태를 번갈아가며 run() 메서드를 조금씩 실행합니다. 실행 상태에서 일시적으로 정지 상태로 가기도 하는데 일시 정지 상태는 WAITING, TIMED_WAITING, BLOCKED가 있습니다.
상태 | 열거 상수 | 설명 |
객체 생성 | NEW | 스레드 객체가 생성, 아직 start() 메서드가 호출되지 않은 상태 |
실행 대기 | RUNNABLE | 실행 상태로 언제든지 갈 수 있는 상태 |
일시정지 | WAITING | 다른 스레드가 통지할 때까지 기다리는 상태 |
일시정지 | TIMED_WAITING | 주어진 시간 동안 기다리는 상테 |
일시정지 | BLOCKED | 사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태 |
종료 | TERMINATED | 실행을 마친 상태 |
실행 중인 스레드의 상태를 변경하는 것을 스레드 상태 제어라고 합니다. 스레드 제어를 제대로 하기 위해서는 스레드의 상태 변화를 가져오는 메서드들을 파악하고 있어야 합니다.
메서드 | 설명 |
interrupt() | 일시 정지 상태의 스레드에서 interruptedException 예외를 발생시켜 예외 처리코드에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 합니다. |
notify() notifyAll() |
동기화 블록 내에서 wait() 메서드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만듭니다. |
resume() | suspend() 메서드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만듭니다. -Deprecated |
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() 메서드를 호출하면 다시 실행 대기 상태가 됩니다. |
yield() | 실행 중에 우선순위가 동일한 다른 스레드에게 실행을 양보하고 실행 대기 상태가 됩니다. |
stop() | 스레드를 즉시 종료시킵니다 - Deprecated |
ThreadA threadA = new ThreadA();
ThreadB threadB = new ThreadB();
threadA.start();
threadB.start();
threadA.work = false // threadB만 실행
threadA.work = true // threadA, threadB 모두 실행
threadA.stop(); // threadA 종료
threadB.stop(); // threadB 종료
public void run() {
while(!stop)
if(work) {
sout("스레드 A 작업 내용")
} else {
Thread.yield // work가 false 되면 다른 스레드에게 양보
}
}
sout("스레드 A 종료")
}
스레드는 다른 스레드와 독립적으로 실행하는 것이 기본이지만 다른 스레드가 종료될 때까지 기다렸다가 실행해야 하는 경우가 발생할 수도 있습니다.
try {
sumThread.join(); // sumThread가 종료할 때까지 메인 스레드를 일시 정지시킴
} catch (InterruptedException e) {
}
두 개의 스레드를 교대로 번갈아가며 실행해야 할 경우가 있습니다. 정확한 교대 작업이 필요할 경우 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고 자신은 일시정지 사애로 만드는 것입니다. 이 방법의 핵심은 공유 객체에 있습니다. 공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메서드로 구분해 놓습니다. 한 스레드가 작업을 완료하면 notify() 메서드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고 자신은 두 번 작업을 하지 않도록 wait() 메서드를 호출하여 일시 정지 상태로 만듭니다. Thread 클래스가 아닌 Object 클래스에 선언된 메소드들로 모든 공유 객체에서 호출이 가능합니다. 주의할 점은 이 메서드들은 동기화 메서드 또는 동기화 블록 내에서만 사용할 수 있습니다.
스레드의 안전한 종료 (stop 플래그, interrupt())
스레드는 자신의 run() 메서드가 모두 실행되면 자동적으로 종료됩니다. 경우에 따라 실행 중인 스레드를 즉시 종료할 필요가 있습니다. Thread는 스레드를 즉시 종료시키기 위해 stop() 메서드를 제공하고 있는데 이 메서드는 deprecated 되었습니다. stop() 메서드로 스레드를 갑자기 종료하면 스레드가 사용중이던 자원들이 불안전한 상태로 남겨지기 때문입니다.
1. stop 플래그를 이용하는 방법
스레드는 run() 메서드가 끝나면 자동적으로 종료되므로 run() 메서드가 정상적으로 종료되도록 유도하는 것이 최선의 방법입니다. 다음 코드는 stop 플래그를 이용해 run() 메서드의 종료를 유도합니다.
public class A extends Thread {
private boolean stop;
public void run() {
while(!stop) {
// 스레드가 반복 실행하는 코드
}
// 스레드가 사용한 자원 정리
}
}
스레드를 종료시키기 위해 setStop() 메서드를 호출
printThread.setStop(true):
public void run() {
while (!stop) {
sout("실행중");
}
sout("자원 정리");
sout("실행 종료");
}
}
2. interrupt() 메서드를 이용하는 방법
interrupt() 메서드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 합니다. 이것을 이용하면 run() 메서드를 정상 종료시킬 수 있습니다.
Thread thread = new PrintThread2();
thread.start();
thread.interrup(); // 스레드를 종료시키기 위해 InterruptedException 발생시킴
데몬 스레드는 주 스레드의 작업을 보조하는 역할을 수행하는 스레드입니다. 주 스레드가 종료되면 데몬 스레드는 강제적으로 자동 종료됩니다. 그 이유는 주 스레드의 보조 역할을 수행하므로 주 스레드가 종료되면 의미가 없어지기 때문입니다.
스레드를 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDaemon(true)를 호출해주면 됩니다.
public static void main(String[] args) {
AutoSaveThread thread = new AutoSaveThread();
thread.setDaemon(true);
thread.start();
}
주의할 점은 start() 메서드가 호출되고 나서 setDaemon(true)를 호출하면 IllegalThreadStateException이 발생하기 때문에 start() 메서드 호출 전에 setDaemon(true)를 호출해야 합니다.
현재 실행 중인 스레드가 데몬인지 확인하기 구분하는 방법은 isDaemon() 메서드의 리턴 값을 보면 됩니다.
람다식 사용법 (0) | 2024.02.02 |
---|---|
제네릭 정리 (0) | 2024.01.31 |
스레드 생성 방법 (Runnable) (0) | 2024.01.29 |
날짜 파싱 메서드 (Formatting) (1) | 2024.01.28 |
Objects 클래스 설명 (0) | 2024.01.26 |
댓글 영역