멀티스레드 환경에서 스레드 실행 순서를 제어하는 것은 동시성 프로그래밍의 핵심 과제입니다. 이 문서에서는 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: 세밀한 제어가 필요하고 상태 기반의 교차 실행이 필요할 때