스레드의 생성과 소멸은 그 자체로 시스템의 부담을 주는 일이다.
처리해야 할 일이 있을 때마다 스레드를 생성하는 것은 성능의 저하로 이어질 수 있다.
그래서 스레드 풀(Thread Pool)
이라는 것을 만들고 미리 제한된 수의 스레드를 생성해 두고 이를 재활용하는 기술을 사용한다.
처리해야 할 작업이 있을 때 풀에서 스레드를 꺼내 작업을 처리한다.
작업을 끝낸 스레드는 다시 풀로 돌아가 다음 작업을 대기하게 된다.
하지만 다음과 같이 스레드를 만들면 스레드는 작업이 종료된 후 소멸한다.
Thread t1 = new Thread(task1);
t1.start() // 스레드는 작업이 끝나면 자동 소멸
따라서 멀티 스레드 프로그래밍에서 스레드 풀의 활용은 매우 중요하다.
스레드 풀의 구현은 concuurent
패키지를 활용하면 된다.
public class ExecutorsDemo {
public static void main(String[] args) {
Runnable task = () -> {
int n1 = 10;
int n2 = 20;
String name = Thread.currentThread().getName();
System.out.println(name + ": " + (n1 + n2));
};
ExecutorService exr = Executors.newSingleThreadExecutor(); // 스레드 풀 생성
exr.submit(task);
System.out.println("End " + Thread.currentThread().getName());
exr.shutdown();
}
}
// 실행 결과
End main
pool-1-thread-1: 30
Executors
클래스의 다음 메소드를 통해서 다양한 유형의 스레드 풀을 생성할 수 있다.
생성된 스레드 풀과 그 안에 존재하는 스레드를 소멸하기 위해서는 다음 메소드를 호출한다.
void shutdown()
다음은 하나의 스레드 풀에 다수의 작업을 전달하는 예다.
public class ExecutorsDemo2 {
public static void main(String[] args) {
Runnable task1 = () -> {
String name = Thread.currentThread().getName();
System.out.println(name + ": " + (5 + 7));
};
Runnable task2 = () -> {
String name = Thread.currentThread().getName();
System.out.println(name + ": " + (7 - 5));
};
ExecutorService exr = Executors.newFixedThreadPool(2);
exr.submit(task1);
exr.submit(task2);
exr.submit(() -> {
String name = Thread.currentThread().getName();
System.out.println(name + ": " + (5 * 7));
});
exr.shutdown();
}
}
// 실행 결과
pool-1-thread-2: 2
pool-1-thread-1: 12
pool-1-thread-2: 35
Runnable 인터페이스 기반으로 스레드 작업을 만들면 반환형이 void이기 대문에 작업의 결과를 return할 수 없다.
Runnable task = () -> {
...
};
그러나 다음 인터페이스를 기반으로 작업을 구성하면 작업의 끝에서 값을 반환하는 것이 가능하다.
특히 반환형도 결정할 수 있다.
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
다음은 Callable
을 대상으로 하는 예다.
public class CallableDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Callable<Integer> task = () -> {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
return sum; // int 반환
};
ExecutorService exr = Executors.newSingleThreadExecutor();
Future<Integer> fur = exr.submit(task);
Integer r = fur.get(); // 스레드 반환 값 획득
System.out.println("result: " + r);
exr.shutdown();
}
}
// 실행 결과
result: 45
Callable
의 반환 값은 Future<V>
형 참조변수에 저장해야 한다.
Future
타입 인자는 Callable
의 타입 인자와 일치시켜야 한다.
자바 5에서 동기화 블록과 동기화 메소드를 대신할 수 있는 ReentrantLock
클래스를 제공한다.
ReentrantLock criticObj = new ReentrantLock();
void myMethod(int arg) {
criticObj.lock(); // 문을 잠근다.
... // 한 스레드에 의해서만 실행되는 영역
criticObj.unlock(); // 문을 연디.
}
먼저 lock 메소드를 호출한 스레드가 unlock 메소드를 호출할 때까지 대기한다.
class Counter {
int count = 0;
ReentrantLock criticObj = new ReentrantLock();
public void increment() {
criticObj.lock();
try {
count++;
} finally {
criticObj.unlock();
}
}
public void decrement() {
criticObj.lock();
try {
count--;
} finally {
criticObj.unlock();
}
}
public int getCount() {
return count;
}
}
public class MultiAccessRenntrantLock {
public static Counter cnt = new Counter();
public static void main(String[] args) throws InterruptedException {
Runnable task1 = () -> {
for (int i = 0; i < 1000; i++) {
cnt.increment();
}
};
Runnable task2 = () -> {
for (int i = 0; i < 1000; i++) {
cnt.decrement();
}
};
ExecutorService exr = Executors.newFixedThreadPool(2);
exr.submit(task1);
exr.submit(task2);
exr.shutdown();
exr.awaitTermination(100, TimeUnit.SECONDS);
System.out.println(cnt.getCount());
}
}
동기화는 성능의 저하를 수반한다.
따라서 불필요하게 동기화를 진행하지 않도록 주의해야 한다.
컬렉션 프레임워크의 클래스 대부분도 동기화 처리가 되어 있지 않다.
Collections
의 다음 메소드들을 통해 동기화를 할 수 있다.
List<String> list = Collections.synchronizedList(new ArrayList<String>());
관련 예제는 다음과 같다.