Java 멀티스레드 제어: 순차, 교차, 동시 실행 패턴 가이드

멀티스레드 환경에서 스레드 실행 순서를 제어하는 것은 동시성 프로그래밍의 핵심 과제입니다. 이 문서에서는 Java에서 제공하는 다양한 동기화 도구를 활용하여 스레드 실행 순서를 제어하는 방법을 세 가지 시나리오로 나누어 설명합니다.

1. 교차 실행 (Alternating Execution)

여러 스레드가 정해진 순서대로 번갈아가며 실행되는 패턴입니다.

1.1 CompletableFuture를 사용한 방식

import java.util.concurrent.CompletableFuture;

public class AlternatingCompletableFuture {
    public static void main(String[] args) {
        for (int cycle = 0; cycle < 5; cycle++) {
            Thread worker1 = new Thread(() -> System.out.println("Worker 1 executed"));
            Thread worker2 = new Thread(() -> System.out.println("Worker 2 executed"));
            Thread worker3 = new Thread(() -> System.out.println("Worker 3 executed"));

            CompletableFuture.runAsync(worker1::start)
                .thenRun(worker2::start)
                .thenRun(worker3::start)
                .join();
        }
    }
}

CompletableFuture의 thenRun() 메서드는 이전 작업이 완료된 후에 다음 작업이 실행되도록 보장합니다.

1.2 synchronized + wait/notify를 사용한 방식

public class AlternatingSync {
    private static String state = "A";
    private static final Object LOCK = new Object();

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                synchronized (LOCK) {
                    while (!state.equals("A")) {
                        try { LOCK.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
                    }
                    System.out.println("Phase A executed");
                    state = "B";
                    LOCK.notifyAll();
                }
            }).start();

            new Thread(() -> {
                synchronized (LOCK) {
                    while (!state.equals("B")) {
                        try { LOCK.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
                    }
                    System.out.println("Phase B executed");
                    state = "C";
                    LOCK.notifyAll();
                }
            }).start();

            new Thread(() -> {
                synchronized (LOCK) {
                    while (!state.equals("C")) {
                        try { LOCK.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
                    }
                    System.out.println("Phase C executed");
                    state = "A";
                    LOCK.notifyAll();
                }
            }).start();
        }
    }
}

각 스레드는 자신의 차례가 올 때까지 대기(wait)하고, 실행 후 상태를 변경한 다음 다른 스레드를 깨웁니다(notifyAll).

2. 동시 실행 (Concurrent Execution)

여러 스레드가 정확히 동시에 실행을 시작해야 하는 경우 사용됩니다.

2.1 CountDownLatch를 사용한 동시 시작

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ConcurrentStart {
    public static void main(String[] args) {
        CountDownLatch startSignal = new CountDownLatch(1);
        ExecutorService threadPool = Executors.newCachedThreadPool();

        for (int i = 0; i < 3; i++) {
            final int workerId = i;
            threadPool.submit(() -> {
                try {
                    startSignal.await();
                    System.out.println("Worker " + workerId + " started at: " + System.currentTimeMillis());
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        System.out.println("Starting all workers simultaneously...");
        startSignal.countDown();
        threadPool.shutdown();
    }
}

CountDownLatch(1)을 사용하면 모든 스레드가 await()에서 대기하다가 countDown() 호출 시 동시에 실행됩니다.

3. 순차 실행 (Sequential Execution)

스레드가 지정된 순서대로 하나씩 실행되도록 제어합니다.

3.1 join() 메서드를 사용한 순차 실행

public class SequentialJoin {
    public static void main(String[] args) {
        Thread task1 = new Thread(() -> System.out.println("Task 1 completed"));
        Thread task2 = new Thread(() -> {
            try {
                task1.join();
                System.out.println("Task 2 completed");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        Thread task3 = new Thread(() -> {
            try {
                task2.join();
                System.out.println("Task 3 completed");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        task1.start();
        task2.start();
        task3.start();
    }
}

3.2 CountDownLatch 체인을 사용한 순차 실행

import java.util.concurrent.CountDownLatch;

class SequentialWorker implements Runnable {
    private final CountDownLatch predecessor;
    private final CountDownLatch successor;

    public SequentialWorker(CountDownLatch predecessor, CountDownLatch successor) {
        this.predecessor = predecessor;
        this.successor = successor;
    }

    @Override
    public void run() {
        try {
            predecessor.await();
            System.out.println("Worker: " + Thread.currentThread().getName() + " executing");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            successor.countDown();
        }
    }
}

public class SequentialCountDown {
    public static void main(String[] args) {
        CountDownLatch latch1 = new CountDownLatch(0);
        CountDownLatch latch2 = new CountDownLatch(1);
        CountDownLatch latch3 = new CountDownLatch(1);

        new Thread(new SequentialWorker(latch1, latch2), "First").start();
        new Thread(new SequentialWorker(latch2, latch3), "Second").start();
        new Thread(new SequentialWorker(latch3, new CountDownLatch(0)), "Third").start();
    }
}

3.3 SingleThreadExecutor를 사용한 순차 실행

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SequentialExecutor {
    public static void main(String[] args) {
        ExecutorService singleExecutor = Executors.newSingleThreadExecutor();

        singleExecutor.submit(() -> System.out.println("First operation"));
        singleExecutor.submit(() -> System.out.println("Second operation"));
        singleExecutor.submit(() -> System.out.println("Third operation"));

        singleExecutor.shutdown();
    }
}

SingleThreadExecutor는 내부 큐를 사용하여 작업을 순차적으로 실행합니다.

3.4 CompletableFuture.thenRun()을 사용한 순차 실행

import java.util.concurrent.CompletableFuture;

public class SequentialCompletableFuture {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Thread step1 = new Thread(() -> System.out.println("Step 1 done"));
            Thread step2 = new Thread(() -> System.out.println("Step 2 done"));
            Thread step3 = new Thread(() -> System.out.println("Step 3 done"));

            CompletableFuture
                .runAsync(step1::start)
                .thenRun(step2::start)
                .thenRun(step3::start)
                .join();
        }
    }
}

각 thenRun() 호출은 이전 CompletableFuture가 완료된 후에만 실행됩니다.

선택 가이드

  • join(): 간단한 순차 실행이 필요할 때, 가장 기본적인 방법
  • CountDownLatch: 여러 스레드의 동시 시작 또는 체인 형태의 순차 실행에 적합
  • SingleThreadExecutor: 작업 큐 기반의 순차 실행이 필요할 때
  • CompletableFuture: 비동기 작업의 복잡한 조합이 필요할 때
  • synchronized + wait/notify: 세밀한 제어가 필요하고 상태 기반의 교차 실행이 필요할 때

태그: java CompletableFuture CountDownLatch Synchronized Thread

6월 9일 20:31에 게시됨