스레드를 생성하는 더 좋은 방법

yanju·2023년 2월 11일
0
post-thumbnail

스레드 풀

스레드의 생성과 소멸은 그 자체로 시스템의 부담을 주는 일이다.

처리해야 할 일이 있을 때마다 스레드를 생성하는 것은 성능의 저하로 이어질 수 있다.

그래서 스레드 풀(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 클래스의 다음 메소드를 통해서 다양한 유형의 스레드 풀을 생성할 수 있다.

  • newSingleThreadExecutor
    • 풀 안에 하나의 스레드만 생성하고 유지한다.
  • newFixedThreadPool
    • 풀 안에 인자로 전달된 수의 스레드를 생성하고 유지한다.
  • newCachedThreadPool
    • 풀 안의 스레드 수를 작업의 수에 맞게 유동적으로 관리한다.

생성된 스레드 풀과 그 안에 존재하는 스레드를 소멸하기 위해서는 다음 메소드를 호출한다.

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

Callable & Future

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의 타입 인자와 일치시켜야 한다.

synchronized를 대신하는 ReentrantLock

자바 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>());

관련 예제는 다음과 같다.

0개의 댓글