멀티 스레드로 작업을 하다보면, 하나의 객체를 공유해서 작업하는 경우가 생긴다. 이 경우, 다른 스레드에 의해 객체 내부 데이터가 쉽게 변경될 수 있기 때문에 의도했던 것과는 다른 결과가 나올 수 있다.
이를 방지하기 위해 필요한 것이 스레드 동기화이다.
public synchronized void method() {
// 단 하나의 스레드만 실행하는 영역
}
동기화 메소드는 synchronized 키워드를 붙여 선언한다. synchronized 키워드는 인스턴스와 정적 메소드 어디든 붙일 수 있다.
스레드가 동기화 메소드를 실행하는 즉시 객체는 잠금이 일어나고, 메소드 실행이 끝나면 잠금이 풀린다. 메소드 전체가 아닌 일부 영역을 실행할 때만 객체 잠금을 걸고 싶다면, 동기화 블록을 만들면 된다.
public void method() {
// 여러 스레드가 실행할 수 있는 영역
synchronized(공유객체) {
// 단 하나의 스레드만 실행하는 영역
}
// 여러 스레드가 실행할 수 있는 영역
}
동기화 블럭은 위 코드와 같이 만들 수 있다.
정확한 교대 작업이 필요한 경우, 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고 자신은 일시 정지 상태로 만들면 된다.
이 방법의 핵심은 공유 객체에 있다.
notify() 메소드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만든다.wait() 메소드를 호출해 일시 정지 상태로 만든다.notify() : wait()에 의해 일시 정지된 스레드 중 한 개를 실행 대기 상태로 만든다.
notifyAll() : wait()에 의해 일시 정지된 모든 스레드를 실행 대기 상태로 만든다.
⚠ 두 메소드는 동기화 메소드 또는 동기화 블록 내에서만 사용할 수 있다.
스레드는 자신의 run() 메소드가 모두 실행되면 자동적으로 종료되지만, 경우에 따라서는 실행 중인 스레드를 즉시 종료할 필요가 있다.
스레드를 강제 종료 시키기 위해 stop() 메소드를 제공하고 있으나, 이 메소드는 deprecated 되었다. 스레드를 갑자기 종료하게 되면 사용 중이던 리소스들이 불안전한 상태로 남겨지기 때문
스레드를 안전하게 종료하는 방법은 사용하던 리소스들을 정리하고 run() 메소드를 빨리 종료하는 것이다.
스레드가 while 문으로 반복 실행할 경우, 조건을 이용해서 run() 메소드의 종류를 유도할 수 있다.
public class XXXThread extends Thread {
private boolean stop;
public void run() {
while (!stop) {
// 스레드가 반복 실행하는 코드
}
}
}
interrupt() 메소드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 한다.
스레드가 실행 대기/실행 상태일 때는 interrupt() 메소드가 호출되어도 예외가 발생하지 않는다. 그러나, 스레드가 어떤 이유로 일시 정지 상태가 되면 예외가 발생한다. 물론 일시저지를 만들지 않고도 interrupt() 메소드 호출 여부를 알 수 있는 방법이 있다.
boolean status = Thread.interrupted();
boolean status = objThread.isInterrupted();
interrupted 메소드는 정적 메소드이고, isInterrupted()는 인스턴스 메소드이다. 두 메소드 모두 interrupt() 메소드 호출 여부를 리턴한다.
데몬 스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다. 주 스레드가 종료되면 데몬 스레드도 따라서 자동으로 종료된다.
스레드를 데몬으로 만들기 위해서는
setDemon(true)를 호출하면 된다.병렬 작업 처리가 많아지면 스레드의 개수가 폭증하여 CPU가 바빠지고 메모리 사용량이 늘어난다. 병렬 작업 증가로 인한 스레드의 폭증을 막으려면 스레드풀을 사용하는 것이 좋다.
-> 작업량이 증가해도 스레드의 개수가 늘어나지 않아 성능이 급격히 저하되지 않는다.
ExecutorService executorService = Executors.newCachedThreadPool();
newCachedThreadPool() 메소드로 생성된 스레드풀의 초기 수와 코어는 0개이고, 작업 개수가 많아지면 새 스레드를 생성시켜 작업을 처리한다. 60초 동안 스레드가 아무 작업을 하지 않으면 스레드를 풀에서 제거한다.ExecutorService executorService = Executors.newFixedThreadPool(5);
newFixedThreadPool()로 생성된 스레드풀의 초기 수는 0개이고, 작업 개수가 많아지면 최대 5개까지 스레드를 생성시켜 작업을 처리한다. 이 스레드풀의 특징은 생성된 스레드를 제거하지 않는다는 것이다.ExecutorService threadPool = new ThreadPoolExecutor(
3, // 코어 스레드 개수
100, // 최대 스레드 개수
120L, // 놀고 있는 시간
TimeUnit.SECONDS, // 놀고 있는 시간 단위
new SynchronousQueue<Runnable>() // 작업 큐
);
위 두 메소드를 사용하지 않고 직접 ThreadPoolExecutor로 스레드풀을 생성할 수도 있다.
스레드풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아 있다.
void shutdown()List<Runnable> shutdownNow()남아 있는 작업을 마무리하고 스레드풀을 종료할 때는 shutdown()을 호출하고, 남아있는 작업과는 상관없이 강제로 종료할 때는 shutdownNow()를 호출하면 된다.