상세 컨텐츠

본문 제목

스레드

백엔드/Java

by Ryuzy 2025. 5. 28. 00:35

본문

728x90
반응형

1. 스레드

스레드(Thread)는 하나의 프로세스 내에서 독립적으로 실행되는 실행 흐름 단위를 말합니다. 일반적으로 하나의 프로그램(프로세스)은 하나 이상의 스레드를 가질 수 있으며, 이를 통해 동시에 여러 작업을 처리하는 멀티스레딩(Multithreading)이 가능합니다. 예를 들어 음악을 재생하면서 동시에 파일을 다운로드하거나 사용자 입력을 처리하는 등의 작업을 병렬로 수행할 수 있습니다. 자바에서는 Thread 클래스를 상속하거나 Runnable 인터페이스를 구현하여 스레드를 정의하고 실행할 수 있으며, 효율적인 CPU 자원 활용과 반응성 향상에 유리하지만, 스레드 간 자원 공유로 인해 동기화(synchronization)와 같은 주의가 필요합니다.

 

 

2. 스레드 생성

1. Thread 클래스를 상속하는 방식

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("스레드 실행 중! - " + Thread.currentThread().getName());
    }
}

public class ThreadExample1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();  // run()이 아닌 start()를 호출해야 새 스레드가 생성됨
    }
}

 

2. Runnable 인터페이스 구현 방식

class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Runnable 스레드 실행 중! - " + Thread.currentThread().getName());
    }
}

public class ThreadExample2 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
    }
}

 

3. 익명 객체로 스레드 생성하는 방법

public class AnonymousThreadExample1 {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                System.out.println("익명 Thread 클래스 실행!");
            }
        };
        t.start();
    }
}

 

public class AnonymousThreadExample2 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("익명 Runnable 객체 실행!");
            }
        });
        t.start();
    }
}

 

public class LambdaThreadExample {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("람다식으로 만든 스레드 실행!");
        });
        t.start();
    }
}

 

 

3. 스레드 병렬 실행

class PrintTask implements Runnable {
    private String message;

    public PrintTask(String message) {
        this.message = message;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(message + " - " + i);
        }
    }
}

public class MultiThreadTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new PrintTask("🍎 김사과"));
        Thread t2 = new Thread(new PrintTask("🍌 반하나"));

        t1.start();
        t2.start();
    }
}

 

 

4. 스레드 동기화 (synchronized)

여러 스레드가 공유 자원을 동시에 수정하면 충돌이 발생할 수 있습니다. synchronized 키워드는 한 번에 한 스레드만 메서드에 접근하도록 합니다.

class Counter {
    private int count = 0;

    public void increment() {
        count++;  // 동시에 접근 시 문제 발생 가능
    }

    public int getCount() {
        return count;
    }
}

 

1. 메서드 전체 동기화

public synchronized void methodName() {
    // 이 메서드는 한 번에 하나의 스레드만 실행 가능
}

 

2. 특정 블록만 동기화

  • 메서드 전체를 동기화하지 않고, 필요한 부분만 동기화 가능
  • 성능 최적화에 좋음
public void methodName() {
    synchronized (this) {
        // this: 현재 인스턴스를 lock
    }
}

 

class SafeCounter {
    private int count = 0;

    public synchronized void increment() {
        count++;  // 메서드에 synchronized → 한 번에 하나의 스레드만 실행
    }

    public int getCount() {
        return count;
    }
}

public class SyncExample {
    public static void main(String[] args) throws InterruptedException {
        SafeCounter counter = new SafeCounter();

        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();  // 안전하게 동작
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("최종 카운트: " + counter.getCount());  // 항상 20000
    }
}

 

join()

Thread.join() 메서드는 다른 스레드가 종료될 때까지 현재 스레드(보통 main 스레드)가 기다리게 만드는 메서드입니다. 즉, t1.join();은 현재 실행 중인 스레드(main 등)가 t1이 끝날 때까지 멈춰서 기다리도록 강제합니다.

 

 

5. ThreadPool

스레드 풀(Thread Pool)은 미리 생성된 스레드들을 풀(pool)에 담아두고, 작업이 생기면 재사용하는 방식입니다. 매번 새 스레드를 생성하는 것이 아니라 재사용하여 성능을 향상시키고 자원 낭비를 줄입니다.

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

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3); // 최대 3개 스레드 사용

        for (int i = 1; i <= 5; i++) {
            int taskId = i;
            executor.submit(() -> {
                System.out.println("작업 " + taskId + "을 실행 중 (스레드: " + Thread.currentThread().getName() + ")");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {}
                System.out.println("작업 " + taskId + " 완료");
            });
        }

        executor.shutdown(); // 작업 제출은 종료 (스레드가 자동 종료되도록)
    }
}

 

 

6. Callable & Future

  • Runnable은 반환값이 없는 작업만 가능
  • Callable<T>은 반환값을 가질 수 있는 작업을 표현
  • Future<T>는 Callable 작업의 결과를 나중에 비동기적으로 받아올 수 있는 객체
import java.util.concurrent.*;

public class CallableFutureExample {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        Callable<Integer> task = () -> {
            System.out.println("복잡한 계산 중...");
            Thread.sleep(2000); // 2초 지연
            return 42; // 결과값 반환
        };

        Future<Integer> future = executor.submit(task);

        System.out.println("메인 스레드는 다른 작업 중...");
        Integer result = future.get();  // 결과가 준비될 때까지 대기

        System.out.println("계산 결과: " + result);
        executor.shutdown();
    }
}
  • submit() → Callable 작업을 실행하고 결과를 Future로 감싸 반환
  • future.get() → 결과가 나올 때까지 대기
  • 비동기 실행이 가능하며, 필요할 때만 결과를 요청

 

 

7. invokeAll() & invokeAny()

1. invokeAll()

여러 Callable<T> 작업을 한꺼번에 제출하고, 모든 작업이 완료될 때까지 대기한 뒤 각 작업의 결과를 List<Future<T>>로 반환합니다.

  • 블록킹: 모든 작업이 종료될 때까지 호출 스레드는 return을 기다립니다.
  • 반환된 Future들을 순회하며 get()을 호출하면 예외 없이 즉시 결과를 얻을 수 있습니다.
  • 타임아웃을 지정할 수도 있습니다(invokeAll(tasks, timeout, unit)).
import java.util.*;
import java.util.concurrent.*;

public class InvokeAllExample {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        // 3개의 Callable 작업 생성
        List<Callable<String>> tasks = List.of(
            () -> { Thread.sleep(500); return "🍎 사과"; },
            () -> { Thread.sleep(300); return "🍌 바나나"; },
            () -> { Thread.sleep(700); return "🍇 포도"; }
        );

        // 모든 작업이 끝날 때까지 대기
        List<Future<String>> futures = executor.invokeAll(tasks);

        // 결과 출력
        for (Future<String> f : futures) {
            try {
                System.out.println("결과: " + f.get());
            } catch (ExecutionException e) {
                System.out.println("작업 중 예외 발생: " + e.getCause());
            }
        }

        executor.shutdown();
    }
}

 

2. invokeAny()

여러 Callable<T> 작업을 제출하고, 가장 먼저 완료된(성공적으로 반환된) 하나의 결과만 리턴합니다. 나머지 작업은 취소됩니다.

  • 가장 빠른 결과만 필요할 때 효율적
  • 블록킹: 최초 성공 작업이 종료될 때까지 대기
  • 타임아웃을 지정할 수도 있습니다(invokeAny(tasks, timeout, unit)).
import java.util.*;
import java.util.concurrent.*;

public class InvokeAnyExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        List<Callable<String>> tasks = List.of(
            () -> { Thread.sleep(500); return "🍎 사과"; },
            () -> { Thread.sleep(300); return "🍌 바나나"; },
            () -> { Thread.sleep(700); return "🍇 포도"; }
        );

        try {
            // 가장 먼저 끝난 작업의 결과만 리턴
            String result = executor.invokeAny(tasks);
            System.out.println("가장 빠른 결과: " + result);
        } catch (InterruptedException | ExecutionException e) {
            System.out.println("예외 발생: " + e.getMessage());
        } finally {
            executor.shutdown();
        }
    }
}

 

 

8. 멀티 점원 햄버거 주문 처리

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

class Order {
    private final int orderId;

    public Order(int orderId) {
        this.orderId = orderId;
    }

    public int getOrderId() {
        return orderId;
    }
}

class OrderProcessor implements Callable<String> {
    private final Order order;
    private final String workerName;
    private static final Random random = new Random();

    // 점원별 처리 속도 범위 (단위: 밀리초)
    private static final Map<String, int[]> workerSpeedMap = Map.of(
        "김사과", new int[]{1000, 2000},  // 빠름
        "반하나", new int[]{2000, 3000},  // 보통
        "오렌지", new int[]{3000, 4000}   // 느림
    );

    private static final AtomicInteger totalProcessed = new AtomicInteger(0);
    private static final Map<String, Integer> workerStats = new ConcurrentHashMap<>();

    public OrderProcessor(Order order, String workerName) {
        this.order = order;
        this.workerName = workerName;
    }

    @Override
    public String call() throws Exception {
        int[] speedRange = workerSpeedMap.get(workerName);
        int prepTime = random.nextInt(speedRange[1] - speedRange[0] + 1) + speedRange[0];
        Thread.sleep(prepTime);

        totalProcessed.incrementAndGet();
        workerStats.merge(workerName, 1, Integer::sum);

        return workerName + " - 주문 " + order.getOrderId() + "번 완료 (소요시간: " + prepTime + "ms)";
    }

    public static int getTotalProcessed() {
        return totalProcessed.get();
    }

    public static void printStats() {
        System.out.println("\n🧾 점원별 처리 주문 수:");
        workerStats.forEach((name, count) -> System.out.println("👨‍🍳 " + name + ": " + count + "건"));
    }
}

public class BurgerOrderSimulator {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        int totalOrders = 10;
        List<String> workers = List.of("김사과", "반하나", "오렌지");

        ExecutorService executor = Executors.newFixedThreadPool(workers.size());
        List<Future<String>> futures = new ArrayList<>();

        for (int i = 1; i <= totalOrders; i++) {
            String worker = workers.get(i % workers.size());
            Order order = new Order(i);
            futures.add(executor.submit(new OrderProcessor(order, worker)));
        }

        for (Future<String> future : futures) {
            System.out.println(future.get());
        }

        executor.shutdown();
        OrderProcessor.printStats();
        System.out.println("총 주문 처리 수: " + OrderProcessor.getTotalProcessed() + "건 🍔");
    }
}
728x90
반응형

'백엔드 > Java' 카테고리의 다른 글

JDBC  (1) 2025.05.29
소켓  (1) 2025.05.28
스트림  (0) 2025.05.25
람다식  (1) 2025.05.25
DTO와 VO  (0) 2025.05.23

관련글 더보기