#Thread란?
Java로 프로그램을 만들다 보면 자연스럽게 마주치는 개념이 프로세스(Process)와 스레드(Thread)다.
처음 접할 때는 단순히 병렬 실행 정도로 이해하기 쉽지만 실제로는 그보다 다양한 상황에 쓰인다.
이번 글에서는 기본 구조부터 직접 구현한 예제까지 한 번에 정리해 본다.
# 프로세스와 스레드의 역할
운영체제에서 프로그램이 실행되면 하나의 프로세스가 만들어진다.
프로세스 안에는 기본적으로 메인 스레드(main thread)가 존재하고
우리가 코드에서 생성하는 스레드는 여기에 추가로 붙는 작업 단위라고 보면 된다.
프로세스(Process): 실행 중인 프로그램 전체
스레드(Thread): 프로세스 내부에서 실제로 작업을 수행하는 흐름(실행 단위)
한 프로세스에는 최소 1개의 메인 스레드가 존재
# Thread의 기본 실행 흐름
스레드는 생성만 한다고 실행되는 게 아니다.
실제로 CPU에서 동작하려면 다음 흐름을 거친다.
start() → Runnable → run() 실행(CPU 점유)
그리고 스레드는 상황에 따라 다음 상태로 이동한다.
| 상태 | 설명 |
| Runnable | 실행 가능 상태(언제든 CPU 잡으면 실행됨) |
| Not Runnable | sleep(), wait(), join() 등에 의해 잠시 멈춤 |
| Terminated | run() 메소드 종료 후 종료 |
wait() → notifyAll()로 깨어나는 구조는 이후에 등장한다.
# Thread 생성 방법
상속하여 만드는 구조
public class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println(Thread.currentThread() + " start");
for (int i = 1; i <= 10; i++) {
System.out.println(i + " ");
}
}
}
실행 코드
class UseT {
public static void main(String[] args) {
var t1 = new MyThread("myThread-1");
var t2 = new MyThread("myThread-2");
t1.start();
t2.start();
}
}
# 예제: 경주마 스레드 게임 만들기
다음은 입력된 말의 숫자만큼 각각 독립적인 스레드가 움직이며
10번 이동하면 결승점을 통과하는 구조이다.
public class Horse implements Runnable {
private String name;
public Horse(String name) { this.name = name; }
@Override
public void run() {
int distance = 0;
Random r = new Random();
for (int i = 0; i < 10; i++) {
distance += 10;
if (distance == 100)
System.out.println(name + " reached~");
else
System.out.println(name + " 이동 : " + distance + "m");
try {
Thread.sleep(r.nextInt(1000) + 1);
} catch (Exception e) {}
}
}
}
실행부
class Main {
public static void main(String[] args) {
System.out.println("게임 말의 개수 입력:");
Scanner sc = new Scanner(System.in);
int num = sc.nextInt();
IntStream.range(0, num)
.forEach(i -> new Thread(new Horse("[" + i + "]")).start());
}
}
# join() - 특정 스레드가 끝날 때까지 대기하기
join()은 다른 스레드가 끝날 때까지 현재 스레드를 잠시 멈추게 하는 기능이다.
public class JoinT {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try { Thread.sleep(2000); } catch (Exception ignored) {}
System.out.println("Thread 1 done");
});
Thread t2 = new Thread(() -> {
try { Thread.sleep(2000); } catch (Exception ignored) {}
System.out.println("Thread 2 done");
});
t1.start();
t2.start();
// main 스레드가 t1/t2 종료를 기다림
t1.join();
t2.join();
System.out.println("모든 작업 종료");
}
}
# 하나의 파일로 합치기 - 싱글 스레드 방식
여러 텍스트 파일을 읽어서 하나의 파일로 합치는 간단한 작업도
스레드를 사용하지 않으면 파일을 순차적으로 읽어서 결과를 만든다.
class FileReadTask {
private List<String> results;
private String fileName;
public FileReadTask(String fileName, List<String> results) {
this.fileName = fileName;
this.results = results;
}
public void read() {
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
String line;
while ((line = br.readLine()) != null)
results.add(line);
} catch (Exception e) {}
}
}
# 멀티 스레드로 파일 병합하기
같은 작업을 여러 스레드가 동시에 파일을 읽도록 개선한 버전이다.
💡포인트
공유 리스트는 동기화 필요 → Collections.synchronizedList()
파일당 1개의 스레드 생성
모든 스레드 종료까지 기다렸다가 파일 쓰기 실행
public class FileReadTask extends Thread {
private final List<String> results;
private final String fileName;
public FileReadTask(String fileName, List<String> results) {
this.fileName = fileName;
this.results = results;
}
@Override
public void run() {
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
String line;
while ((line = br.readLine()) != null)
results.add(line);
} catch (Exception e) {}
}
}
메인 코드
class FileReaderMain {
public static void main(String[] args) {
List<String> results = Collections.synchronizedList(new ArrayList<>());
String[] files = {"file1.txt", "file2.txt", "file3.txt"};
List<Thread> threads = new ArrayList<>();
for (String file : files) {
var task = new FileReadTask(file, results);
task.start();
threads.add(task);
}
// 모든 파일 읽기 완료될 때까지 대기
for (Thread t : threads) {
try { t.join(); } catch (Exception ignored) {}
}
try (BufferedWriter bw = new BufferedWriter(new FileWriter("result.txt"))) {
for (String line : results) {
bw.write(line);
bw.newLine();
}
System.out.println("파일 병합 완료");
} catch (Exception e) {}
}
}
# wait() / notifyAll() - Producer–Consumer 구조 구현
이제 동기화에서 가장 중요한 기능인 wait()와 notifyAll()을 사용해 보자.
예시는 도서관 좌석 3개를 학생 6명이 공유하는 시나리오로 만들었다.
💡포인트
자리가 없으면 wait() → 스레드 잠시 대기
누군가 자리를 반납하면 notifyAll() → 대기 중인 스레드 모두 깨움
좌석 관리는 synchronized 블록 내에서만 가능
public class Library {
private static Library Instance;
private List<String> seats;
private Library() {
seats = new ArrayList<>(Arrays.asList("1번 자리", "2번 자리", "3번 자리"));
}
public synchronized static Library getInstance() {
if (Instance == null) Instance = new Library();
return Instance;
}
public synchronized String study() throws InterruptedException {
if (seats.isEmpty()) {
System.out.println(Thread.currentThread().getName() + " wait!");
wait();
}
if (!seats.isEmpty()) {
var seat = seats.remove(0);
System.out.println("Use seat = " + seat);
return seat;
}
return null;
}
public synchronized void returnSeat(String seat) {
seats.add(seat);
notifyAll();
System.out.println(Thread.currentThread().getName() + " returned: " + seat);
}
}
학생 스레드
class Student extends Thread {
public void run() {
try {
String seat = Library.getInstance().study();
if (seat == null) return;
sleep(2000);
Library.getInstance().returnSeat(seat);
} catch (Exception e) {}
}
}
실행
class UseApp {
public static void main(String[] args) {
IntStream.range(0, 6).forEach(i -> new Student().start());
}
}'java' 카테고리의 다른 글
| Stream (0) | 2025.11.28 |
|---|---|
| Lambda Expression(=람다식) (0) | 2025.11.28 |
| Generic Type(=제네릭 타입) (0) | 2025.11.28 |
| Abstract class(=추상화) (0) | 2025.11.28 |
| Encapsulation(=캡슐화) (0) | 2025.11.28 |
