프로그램을 실행하면 프로세스가 되는데 프로세스는 최소 하나의 스레드가 있어야 하며 따라서 최소 하나의 스택메모리가 있어야한다.
| 구분 | 특징 |
|---|---|
| 싱글 스레드 | main()만 실행 → 순차적으로 흐름 1개 |
| 멀티 스레드 | 여러 흐름이 동시에 존재 → 누가 먼저 끝날지 예측 불가 |
멀티 스레드의 예시로 카카오톡에서 새로운 메세지를 받으면서 타이핑을 칠 수 있는 것을 떠올리면 좋다.


| 개념 | 설명 |
|---|---|
| 동시성(Concurrency) | CPU 1개가 여러 작업을 번갈아가며 처리 → 동시에 하는 것처럼 ‘보여짐’ |
| 병렬성(Parallelism) | CPU 여러 개가 여러 작업을 실제로 동시에 처리 |
Java 에서 스레드를 만드는 대표적인 3가지 방식은 아래와 같다.
| 방식 | 설명 |
|---|---|
| Task_1 | Thread 클래스를 직접 상속 |
| Task_2 | Runnable 인터페이스 구현 → Thread 생성자에 전달 |
| 익명 구현 | Runnable 익명객체를 바로 new Thread() 에 넣는 방식 |
왜 방식이 나뉘었을까?
→ 자바는 “다중 상속 불가” 이기 때문이다.
이미 다른 클래스를 상속 중인 상태에서는 Thread 를 직접 상속할 수 없으므로
Runnable 을 구현하도록 설계되어 있다.
실제 스레드 실행은 main() 에서 start() 로 시작한다.
/*
Thread : 프로세스에서 하나의 최소 실행단위(흐름)
run() : Thread 의 main 역할, 새로 생성된 stack 의 첫 프레임에 올라감
*/
class Task_1 extends Thread { // 1) Thread 직접 상속
@Override
public void run() {
for(int i = 0 ; i < 100 ; i++){
System.out.println("Task_1 : " + i + " >> " + this.getName());
}
System.out.println("Task_1 run() 함수 END");
}
}
class Task_2 implements Runnable { // Thread 가 아니다. run()만 가지고 있는 클래스.
@Override
public void run() {
for(int i = 0 ; i < 100 ; i++){
System.out.println("Task_2 : " + i);
}
System.out.println("Task_2 run() 함수 END");
}
}
public class Ex02_Multi_Thread {
public static void main(String[] args) {
// 1) Thread 상속 사용
Task_1 t1 = new Task_1();
// 2) Runnable 구현 사용
Task_2 task2 = new Task_2();
Thread t2 = new Thread(task2); // Runnable 은 Thread 에 넣어서 실행해야 실제 스레드 됨
// 3) 익명객체 사용
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0 ; i < 100 ; i++){
System.out.println("Task_3 : " + i + " >> " + Thread.currentThread().getName());
}
System.out.println("Task_3 run() 함수 END");
}
});
// 실행 (run() 직접 호출 X, 반드시 start())
t1.start();
t2.start();
t3.start();
// main thread
for(int i = 0 ; i < 100 ; i++){
System.out.println("main : " + i);
}
System.out.println("main END");
}
}
| 메서드 | 역할 |
|---|---|
| run() | 스레드가 실제로 수행할 코드 |
| start() | JVM 에게 “새 스레드 하나 만들어서 run() 올려주세요” 라고 요청 |
run() 은 원래 Runnable 인터페이스에서 추상 메서드였다.
하지만 Thread 클래스가 기본 구현을 이미 제공하고 있기 때문에 Thread 의 run() 은 abstract 가 아니다.
즉 run() 은 “반드시 오버라이드하도록 강제하는 함수가 아니다.”
그리고 run() 을 직접 호출하면 단순 메서드 호출일 뿐이며
새로운 스레드는 생성되지 않는다 → 그래서 반드시 start() 를 써야 한다.
세 개의 스레드(t1 / t2 / t3) 중 누가 먼저 끝날까?
정답: 알 수 없다.
실행할 때마다 결과는 달라진다.
왜냐하면 CPU 를 점유했다가 뺏기는 과정을 OS 스케줄러가 결정하기 때문이다.
즉 아래 스샷처럼 출력 순서는 계속 랜덤하게 섞인다:

멀티스레드는 동작과 종료 순서를 개발자가 결정할 수 없다.
OS 스케줄링에 따라 CPU 점유가 계속 바뀌기 때문이다.
package org.basic.util;
import javax.swing.JOptionPane;
public class MultiThread_GuGuDan {
static boolean timeOut = false;
public static void main(String[] args) {
int totalCount = 0; // 총 문제 수
int correctCount = 0; // 정답 수
Thread timer = new Thread(() -> {
for(int i = 10; i > 0; i--){
System.out.println("남은시간 : " + i);
try { Thread.sleep(1000); } catch (Exception e){}
}
timeOut = true;
System.out.println("시간 종료!");
});
timer.start();
while(!timeOut) { // 시간 끝날때까지 계속 문제!
int dan = (int)(Math.random()*8)+2;
int num = (int)(Math.random()*9)+1;
int result = dan * num;
String input = JOptionPane.showInputDialog(dan + " x " + num + " = ?");
if(timeOut) break; // 입력도중 시간 종료 체크
if(input == null || input.equals("")) continue;
totalCount++;
if(Integer.parseInt(input) == result){
System.out.println("정답!");
correctCount++;
} else {
System.out.println("오답!");
}
}
// 여기서 최종 결과 출력
System.out.println("========================");
System.out.println("게임 종료");
System.out.println("총 " + totalCount + "문제 중 " + correctCount + "문제 정답!");
System.out.println("========================");
}
}

멀티스레드에서 가장 중요한 키워드는 공유 자원이다.
벡터는 동기화를 보장하지만,
arrayList는 동기화를 보장하지 않으므로,
값의 변화를 인지할 수 없지만 성능에 유리하다.
예를 들어보자.
한강에서 비빔밥 축제가 열렸다고 해보자.
가운데 커다란 솥에 비빔밥이 산처럼 쌓여 있고
사람들이 둥글게 둘러앉아 한 숟가락씩 퍼먹는다.
이 때 공유 자원은 비빔밥이다.
다 같이 막 퍼먹기 시작하면
누가 많이 퍼갔는지 / 누가 적게 퍼갔는지 / 순서가 뒤섞이든
상관 없이 그냥 빠르게 먹을 수 있다.
→ 즉 “락이 없는 상태”
이번엔 다같이 비빔밥을 너~무 많이 먹어서 화장실을 찾았다.
친구들과 한강에 놀러 갔는데 화장실 칸이 딱 한 칸만 있다고 하자.
이 때 공유 자원은 변기라는 아주 직관적인 대상이다.
사람은 한 명씩 들어가서 문을 잠근다.
문을 잠근다는 건 락(lock) 을 걸었다는 의미다.
이 case 는 누가 먼저 들어가 몇 초를 쓰는지가 아주 명확해진다.
대신 변비 환자가 들어가 있으면 뒤에 줄이 길어질 수 있다.
이게 바로 “동기화(synchronization)”의 trade-off 이다.
동기화는 공유자원에 대한 접근 질서를 보장하기 위한 기능이다.
자바에서는 이런 동기화를 synchronized 키워드 하나로 간단히 적용할 수 있다.
synchronized : 공유자원에 한 번에 한 스레드만 접근하도록 락을 거는 키워드
package org.basic.util;
class Wroom {
synchronized void openDoor(String name) {
System.out.println(name + "님 화장실 입장 ^^");
for (int i = 0; i < 100; i++) {
System.out.println(name + "사용 중" + i);
if (i == 10) {
System.out.println(name + "님이 💩");
}
}
System.out.println("시원");
}
}
class User extends Thread {
Wroom wr;
String who;
User(String name, Wroom wr) {
this.who = name;
this.wr = wr;
}
@Override
public void run() {
wr.openDoor(this.who);
}
}
public class SyncThread {
public static void main(String[] args) {
// 여기는 한강 화장실
Wroom w = new Wroom();
User kim = new User("김씨", w);
User Lee = new User("이씨", w);
User Park = new User("박씨", w);
kim.start();
Lee.start();
Park.start();
}
}