스레드

현서·2025년 5월 30일
1

자바

목록 보기
21/32
post-thumbnail

1. 스레드(Thread)

  • 하나의 프로세스 내에서 독립적으로 실행되는 실행 흐름 단위.
  • 일반적으로 하나의 프로그램(프로세스)은 하나 이상의 스레드를 가질 수 있다.
    -> 동시에 여러 작업을 처리하는 멀티스레딩(Multithreading)이 가능하다.

2. 스레드 생성

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

package lesson08;

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

public class Ex06_Main {
    public static void main(String[] args) {
        Ex06_MyThread t1 = new Ex06_MyThread();
        t1.start();
    }
}

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

package lesson08;

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

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

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

package lesson08;

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

3. 스레드 병렬 실행

package lesson08;

class Ex09_PrintTask implements Runnable{
    private String message;

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

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

public class Ex09_Main {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Ex09_PrintTask("김사과"));
        Thread t2 = new Thread(new Ex09_PrintTask("반하나"));
        Thread t3 = new Thread(new Ex09_PrintTask("오렌지"));
        t1.start();
        t2.start();
        t3.start();
    }
}

스레드 스케줄링은 OS가 결정하므로 매번 순서가 달라진다.

김사과 - 0
반하나 - 0
오렌지 - 0
김사과 - 1
오렌지 - 1
반하나 - 1
김사과 - 2

같은 숫자끼리 줄 맞춰 나오지 않는 이유 = context switching. CPU가 세 스레드를 번갈아 가며 실행하기 때문이다.

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
    }
}
package lesson08;

class Ex10_Counter{
    private int count = 0;

    public synchronized void increment(){
        count++;
    }

    public int getCount(){
        return count;
    }
}

public class Ex10_Main {
    public static void main(String[] args) throws InterruptedException{
        Ex10_Counter counter = new Ex10_Counter();
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++){
                counter.increment();
            }
        };
        Thread th1 = new Thread(task);
        Thread th2 = new Thread(task);
        th1.start();
        th2.start();
        th1.join();
        th2.join();
        System.out.println("최종 카운트: " + counter.getCount());
    }
}

join()

Thread.join() 메서드 : 다른 스레드가 종료될 때까지 현재 스레드(보통 main 스레드)가 기다리게 만드는 메서드.

5. ThreadPool(Thread Pool)

  • 미리 생성된 스레드들을 풀(pool)에 담아두고, 작업이 생기면 재사용하는 방식.
  • 스레드를 재사용하여 성능을 향상시키고 자원 낭비를 줄인다.
package lesson08;

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

public class Ex11_Main {
    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 작업의 결과를 나중에 비동기적으로 받아올 수 있는 객체
package lesson08;

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

public class Ex12_Main {
    public static void main(String[] args) throws Exception{
        ExecutorService executor = Executors.newSingleThreadExecutor();
        // 하나의 스레드만 사용하는 ExecutorService를 생성

        Callable<Integer> task = () ->{
            System.out.println("복잡한 계산 중...");
            Thread.sleep(2000);
            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)).
package lesson08;

import java.util.List;
import java.util.concurrent.*;

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

        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. 멀티 점원 햄버거 주문 처리

package lesson08;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

class Ex14_Order{
    private final int orderId;

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

    public int getOrderId(){
        return orderId;
    }
}

class Ex14_OrderProcessor implements Callable<String>{
    private final Ex14_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 Ex14_OrderProcessor(Ex14_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);
        // 주문 1건이 처리되었으니, 전체 주문 수를 1 증가
        totalProcessed.incrementAndGet();
        // workerName: 아르바이트생 이름
        // 1: 처리한 주문 수
        // 아르바이트생 이름이 Map에 없으면 (workerName, 1)로 추가
        // 이미 있다면 ("김사과", 기존값 + 1) 로 업데이트
        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("점원별 처리 주문 수: ");
        workerStats.forEach((name, count)-> System.out.println(name + ": " + count + "건"));
    }

}

public class Ex14_Main {
    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());
            Ex14_Order order = new Ex14_Order(i);
            futures.add(executor.submit(new Ex14_OrderProcessor(order, worker)));
        }

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

        executor.shutdown();
        Ex14_OrderProcessor.printStats();
        System.out.println("총 주문 처리 수: " + Ex14_OrderProcessor.getTotalProcessed() + "건");
    }
}
profile
The light shines in the darkness.

0개의 댓글