Java 멀티스레드 | Thread, Runnable, Executor

Java 멀티스레드 | Thread, Runnable, Executor

이 글의 핵심

Java 멀티스레드에 대해 정리한 개발 블로그 글입니다. class MyThread extends Thread { private String name; public MyThread(String name) { this.name = name; } @Override…

들어가며

Thread·Executor·synchronized 등으로 여러 실행 흐름을 다룹니다. 공유 객체에 대한 접근 순서를 정하지 않으면 데이터가 깨질 수 있으므로, 락·원자 변수·동시성 유틸과 함께 읽는 것이 좋습니다.

C++ std::thread·mutexOS 스레드 + 락이라는 큰 그림이 비슷해 비교하기 좋고, Go 고루틴처럼 스레드보다 가벼운 단위를 쓰는 언어와는 설계 선택이 다릅니다.


1. Thread 생성

방법 1: Thread 상속

class MyThread extends Thread {
    private String name;
    
    public MyThread(String name) {
        this.name = name;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + ": " + i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread t1 = new MyThread("Thread-1");
        MyThread t2 = new MyThread("Thread-2");
        
        t1.start();
        t2.start();
    }
}

방법 2: Runnable 구현 (권장)

class MyRunnable implements Runnable {
    private String name;
    
    public MyRunnable(String name) {
        this.name = name;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + ": " + i);
        }
    }
}

// 사용
Thread t1 = new Thread(new MyRunnable("Thread-1"));
Thread t2 = new Thread(new MyRunnable("Thread-2"));

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

// 람다로 더 간결하게
Thread t3 = new Thread(() -> {
    for (int i = 0; i < 5; i++) {
        System.out.println("Thread-3: " + i);
    }
});
t3.start();

2. ExecutorService

스레드 풀

import java.util.concurrent.*;

public class ExecutorExample {
    public static void main(String[] args) {
        // 고정 크기 스레드 풀
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " by " + 
                    Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        
        executor.shutdown();
        
        try {
            executor.awaitTermination(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Future와 Callable

import java.util.concurrent.*;

public class FutureExample {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        
        // Callable: 반환값 있음
        Callable<Integer> task = () -> {
            Thread.sleep(1000);
            return 42;
        };
        
        Future<Integer> future = executor.submit(task);
        
        System.out.println("작업 진행 중...");
        
        // 결과 대기
        Integer result = future.get();  // 블로킹
        System.out.println("결과: " + result);
        
        executor.shutdown();
    }
}

3. 동기화 (Synchronization)

synchronized 메서드

class Counter {
    private int count = 0;
    
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        
        System.out.println("Count: " + counter.getCount());  // 2000
    }
}

synchronized 블록

class BankAccount {
    private int balance = 0;
    private final Object lock = new Object();
    
    public void deposit(int amount) {
        synchronized (lock) {
            balance += amount;
        }
    }
    
    public void withdraw(int amount) {
        synchronized (lock) {
            if (balance >= amount) {
                balance -= amount;
            }
        }
    }
    
    public int getBalance() {
        synchronized (lock) {
            return balance;
        }
    }
}

4. 실전 예제

예제: 병렬 다운로더

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

public class ParallelDownloader {
    public static void main(String[] args) throws Exception {
        List<String> urls = Arrays.asList(
            "https://example.com/file1.txt",
            "https://example.com/file2.txt",
            "https://example.com/file3.txt"
        );
        
        ExecutorService executor = Executors.newFixedThreadPool(3);
        List<Future<String>> futures = new ArrayList<>();
        
        for (String url : urls) {
            Future<String> future = executor.submit(() -> {
                System.out.println("다운로드 시작: " + url);
                Thread.sleep(1000);  // 다운로드 시뮬레이션
                return "완료: " + url;
            });
            futures.add(future);
        }
        
        for (Future<String> future : futures) {
            System.out.println(future.get());
        }
        
        executor.shutdown();
    }
}

정리

핵심 요약

  1. Thread: 스레드 생성, start()로 실행
  2. Runnable: 작업 정의, 람다 사용 가능
  3. ExecutorService: 스레드 풀, 재사용
  4. Future/Callable: 반환값 있는 작업
  5. synchronized: 동기화, 데이터 무결성

다음 단계

  • Java Spring Boot

관련 글

  • Java 시작하기 | JDK 설치부터 Hello World까지
  • Java 변수와 타입 | 기본 타입, 참조 타입, 형변환