효율적으로 성능 최적화하기 위해, 멀티 스레드 환경에서 무엇을 고려해야 하는지 명확히 이해하기 위함
즉, 멀티 스레드 환경에서 발생할 수 있는 문제를 명확히 이해하고, 이를 해결하기 위한 가장 적절한 방안을 제시하는 주관을 갖기 위함이다
따라서 스레드의 개념부터 시작해 JVM 환경에서 스레드 활용 시 고려해야 할 사항, 성능 최적화 기법까지 정리한다
스레드란?
🤔 스레드를 비유하자면?
"스레드는 프로세스라는 커다란 식당에서 일하는 요리사다"
쉽게 표현하기 위해 스레드를 식당의 요리사로 비유할 수 있다
컴퓨터에서 실행되는 프로그램을 하나의 큰 식당으로 생각해 보자 (프로세스 = 큰 식당) (식당은 손님들에게 요리를 제공하기 위해 오픈(실행)되었다)
식당에는 손님(요청)이 계속해서 들어오고, 요리사(스레드)들이 각자 맡은 일을 처리한다 (요리사들 = 스레드들)
싱글 스레드는 요리사 한 명이 모든 요리를 순서대로 만드는 방식이다 (주문이 많아지면 요리사가 감당하지 못해 대기 시간이 길어진다)
멀티 스레드는 여러 명의 요리사가 각자 맡은 요리를 동시에 조리하는 방식이다 (여러 요리를 한 번에 처리할 수 있어 속도가 빨라진다)
하지만 요리사들이같은 재료(공유 자원)를 동시에 사용하려고 하면 문제(경합)가 발생할 수 있다
이를 방지하기 위해 요리사들끼리 서로 협력하는 규칙(동기화, 락)이 필요하다
위와 같이 스레드는 하나의 프로세스 안에서 여러 작업을 동시에 수행할 수 있도록 도와주는 실행 단위다
프로그램이 실행되면 운영체제는 프로세스를 생성하고, 프로세스 내부에서 특정 작업을 수행하기 위해 스레드를 만든다
하나의 프로세스는 최소 하나의 스레드를 가지며(메인 스레드), 여러 개의 스레드를 생성하여 병렬 처리를 할 수도 있다
스레드는 같은 프로세스 내의 메모리 공간(Code, Data, Heap)을 공유하지만, 스택(Stack)은 각각 독립적으로 갖는다
멀티 스레딩을 활용하면 여러 작업을 동시에 수행할 수 있지만, 동기화 문제, 데드락, 컨텍스트 스위칭 비용과 같은 고려해야 할 요소도 존재한다
즉, 스레드는프로세스 내에서 실행되는 독립적인 작업 흐름이며, 멀티태스킹을 가능하게 하는 중요한 개념이다
프로세스 VS 스레드
위 비유와 같이 프로세스는 독립적인 식당이며, 스레드는 그 식당에서 일하는 직원(요리사)과 같다 즉, 프로세스는 OS가 관리하는 독립적인 실행 단위이고, 스레드는 그 내부에서 실행되는 실행 흐름이다
각 식당들은 서로 독립적이다 => 각 프로세스는 서로 독립적이며, 고유한 자원(메모리, 파일 등)을 가진다
프로세스 = 하나의 식당 → 각 식당은 자신의 주방, 재료, 주문 시스템을 따로 가지고 있음 → 다른 식당과는 자원을 공유하지 않음 → 식당(프로세스) 간에 협업하려면 ‘전화 주문’ 같은 별도의 통신(IPC, Inter-Process Communication)이 필요함
스레드 = 식당의 직원 → 같은 식당에서 일하는 직원들은 공용 냉장고(메모리), 주방(데이터 영역)을 함께 사용함 → 하지만 직원(스레드)마다 자기만의 작업 공간(스택)은 따로 있음 → 직원이 많아지면(멀티 스레드) 일을 빠르게 처리할 수 있지만, 같은 냉장고(공유 자원)를동시에 사용하면 충돌이 발생할 수 있음
위 비유와 같이 프로세스는 운영체제(OS)로부터 독립적인 메모리 공간(Code, Data, Heap, Stack)을 할당받아 실행된다
스레드는 프로세스 내부에서 실행되는 작업 흐름이며, 같은 프로세스의 메모리(Code, Data, Heap)를 공유하지만 Stack은 개별적으로 가진다
멀티 프로세스는 여러 개의 프로세스를 실행하여 작업을 처리하는 방식이며, 프로세스 간 통신(IPC)이 필요하므로 오버헤드가 크다
멀티 스레드는 하나의 프로세스 내에서 여러 스레드를 실행하는 방식이며, 메모리를 공유하여 효율적이지만 동기화 문제가 발생할 수 있다
유저 스레드 VS 커널 스레드
"유저 스레드가 무대 위에서 공연하는배우라면, 커널 스레드는 무대 뒤에서 일하는스태프다."
우리가 극장에서 연극을 볼 때, 배우들은 무대 위에서 공연을 펼치고 있지만, 무대 뒤에서는 조명팀, 음향팀, 무대 감독 등이 공연이 원활하게 진행되도록 지원하고 있다
유저 스레드(User Thread) = 배우 → 애플리케이션이 직접 관리하는 스레드 → 운영체제의 개입 없이 실행되며, 성능적으로 유리하지만 시스템적인 보호가 부족함 → 배우(유저 스레드)가 공연 중 쓰러지면, 무대 뒤(커널 스레드)의 지원 없이는 정상적으로 공연을 마치기 어려움 즉, 가볍고, 성능적으로 유리하지만, 시스템 콜을 직접 수행할 수 없고, 커널의 지원이 필요함
커널 스레드(Kernel Thread) = 무대 뒤 스태프 → 운영체제가 직접 관리하는 스레드 → 배우(유저 스레드)의 작업을 지원하며, 스케줄링, 리소스 관리 등의 역할을 수행 → 무대 뒤에서 동작하므로, 운영체제의 개입이 필요하고 컨텍스트 스위칭 비용이 발생할 수 있음 즉, 유저 스레드의 작업을 처리하며, 시스템 콜을 수행할 수 있음
👉 즉, 유저 스레드는 애플리케이션이 직접 다루는 스레드이고, 커널 스레드는 운영체제가 관리하는 스레드다. 유저 스레드는 커널 스레드 없이는 실행될 수 없으며, 커널 스레드는 유저 스레드의 동작을 지원한다.
✅ 스레드의 장점
👍🏻 병렬 처리 (Parallel Processing)
“여러 명이 함께 요리하면 음식이 더 빨리 완성된다”
싱글 스레드는 마치 1명이 요리를 하는 것과 같다 한 사람이 재료 손질 → 조리 → 플레이팅을 순차적으로 해야 해서 주문이 밀렸을 때, 시간이 오래 걸린다
하지만 여러 사람이 역할을 나눠 병렬로 작업하면 요리 속도가 훨씬 빨라지듯, 멀티 스레드를 활용하면 여러 작업을 동시에 처리하여 실행 속도를 향상시킬 수 있다
📌 예시
웹 서버에서 여러 사용자의 요청을 동시에 처리할 때
동영상 편집 소프트웨어에서 여러 필터 효과를 동시에 적용할 때
게임 엔진에서 물리 연산, 렌더링, AI 계산을 병렬로 수행할 때
🟢 [1부터 10만까지 더하는 연산] 싱글 스레드 VS 멀티 스레드
public class MultiThreadSumExample {
public static void main(String[] args) throws InterruptedException {
int start = 1;
int mid = 50_000;
int end = 100_000;
// 🟢 1. 싱글 스레드로 실행
long startTime = System.nanoTime();
long singleThreadSum = sumRange(start, end);
long singleThreadTime = System.nanoTime() - startTime;
System.out.println("Single Thread Sum: " + singleThreadSum);
System.out.println("Single Thread Time: " + singleThreadTime / 1_000_000.0 + " ms");
// 🟢 2. 멀티 스레드로 실행
SumThread thread1 = new SumThread(start, mid);
SumThread thread2 = new SumThread(mid + 1, end);
startTime = System.nanoTime();
thread1.start();
thread2.start();
thread1.join(); // Thread-1이 끝날 때까지 대기
thread2.join(); // Thread-2가 끝날 때까지 대기
long multiThreadTime = System.nanoTime() - startTime;
long multiThreadSum = thread1.getResult() + thread2.getResult();
System.out.println("Multi Thread Sum: " + multiThreadSum);
System.out.println("Multi Thread Time: " + multiThreadTime / 1_000_000.0 + " ms");
}
// 🟢 싱글 스레드 합 계산 함수
public static long sumRange(int start, int end) {
long sum = 0;
for (int i = start; i <= end; i++) {
sum += i;
}
return sum;
}
}
// 🟢 멀티 스레드용 계산 클래스
class SumThread extends Thread {
private final int start, end;
private long result;
public SumThread(int start, int end) {
this.start = start;
this.end = end;
}
public void run() {
result = MultiThreadSumExample.sumRange(start, end);
}
public long getResult() {
return result;
}
}
Single Thread Sum: 5000050000
Single Thread Time: 3.2 ms
Multi Thread Sum: 5000050000
Multi Thread Time: 2.1 ms
1부터 10만까지 덧셈을 단일 스레드로연산한 시간: 3.2 ms
1부터 5만까지 덧셈, 5만1부터 10만까지 덧셈을 멀티 스레드로 동시에 연산하여 합산한 시간: 2.1 ms
위 결과와 같이 스레드 생성 비용도 존재하며 정확히 2배의 효율은 아니나, 처리 시간 약 1.5배 가량 차이가 발생했다
👍🏻 자원 공유 & 빠른 컨텍스트 스위칭
“한 집에서 형제들이 냉장고를 공유하면, 효율적으로 식재료를 공유할 수 있다”
멀티 스레드는 같은 프로세스의 메모리 공간(코드, 힙, 데이터 영역)을 공유하기 때문에, 서로 정보를 주고받거나 자원을 활용할 때 효율적이다
만약 여러 개의 프로세스를 사용했다면, 프로세스 간 통신(IPC, Inter-Process Communication) 비용이 발생하지만, 스레드는 동일한 메모리 공간을 사용하기 때문에 데이터 공유가 쉽고 빠르다
또한, 컨텍스트 스위칭 비용도 프로세스보다 적게 든다 WHY? 프로세스 간 전환할 때는 CPU가 전체 메모리 맵을 변경 해야 하지만, 스레드 간 전환은 스택과 레지스터 정보만 바꾸면 되므로 오버헤드가 적다
📌 예시
웹 브라우저에서 여러 개의 탭을 띄워 놓아도 빠르게 전환 가능
멀티플레이어 온라인 게임에서 여러 캐릭터가 같은 맵을 공유하며 동시에 활동 가능
public class MultiThread {
private static final int sharedValue = 100; // 공유 자원 (힙 영역)
public static void main(String[] args) {
Thread thread1 = new Thread(() -> System.out.println("Thread 1: sharedValue = " + sharedValue));
Thread thread2 = new Thread(() -> System.out.println("Thread 2: sharedValue = " + sharedValue));
thread1.start();
thread2.start();
}
}
위와 같이 여러 스레드에 공유될 수 있는 자원(sharedValue)을 활용하여 효율적으로 정보를 주고받을 수 있다
❌ 스레드의 단점
👎🏻 동기화 문제 (Synchronization Issues)
“여러 명이 한 노트에 동시에 글을 쓰면, 글자가 엉망이 된다.”
스레드는 메모리를 공유하기 때문에 동시에 같은 데이터를 수정하면 데이터 충돌(Race Condition)이 발생할 수 있다 (당연히 자원 공유에 따른 장점도 크지만)
예를 들어, 두 개의 스레드가 은행 계좌의 잔액을 수정한다고 가정하면, 스레드 A가 잔액을 읽고 100원을 추가하려는 순간, 스레드 B가 같은 잔액을 읽고 200원을 추가한다면? 결과적으로 총 300원이 추가되는게 아닌, 100원이나 200원만 추가되어 예상치 못한 결과(데이터 불일치)가 발생할 수 있다
이를 방지하려면 synchronized, Lock, Atomic 클래스 활용과 같은 동기화 기법을 사용해야 하지만, 동기화를 잘못하면 데이터 불일치 문제가 해결이 안 되거나, 성능 저하가 발생할 수도 있다
📌 예시
멀티스레드 환경에서 하나의 파일을 동시에 수정하는 경우
은행 시스템에서 여러 사용자가 같은 계좌의 잔액을 수정하는 경우
public class RaceCondition {
private static int count = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> count++);
Thread t2 = new Thread(() -> count++);
t1.start();
t2.start();
System.out.println(count); // 예상과 다를 수 있음 (Race Condition)
}
}
`count++`는 읽기 → 증가 → 쓰기 단계로 나뉘는데, 두 스레드가 동시에 접근하면 값이 꼬일 수 있다
즉, 예상한 값(2)이 나오지 않을 수도 있음
👎🏻 데드락 (Deadlock)
“서로 길을 양보하지 않는 두 대의 차가 교차로에서 멈춰버린 상황” (제발 서로 차 빼..)
위의 동시성 문제를 해결하기 위해,멀티 스레드는 여러 개의 락(Lock)을 사용해 데이터를 보호한다 (즉, 동시성 문제가 애초에 발생되지 않은 조건이면 Lock을 걸 이유도 없기 때문에 데드락도 존재해선 안 된다)
만약 스레드 A가 자원 X를 점유한 상태에서 자원 Y를 기다리고, 스레드 B가 자원 Y를 점유한 상태에서 자원 X를 기다리면 서로 대기만 하다가 멈춰버리는 현상(데드락)이 발생할 수 있다
📌 예시
ATM 시스템에서 한 사용자가 출금을 시도하는 동시에, 다른 사용자가 계좌 이체를 시도할 경우
교착 상태에 빠진 철도 시스템 (서로 반대 방향으로 진행하는 기차가 교차로에서 멈춰 있음)
public class DeadlockExample {
static final Object A = new Object();
static final Object B = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> { synchronized (A) { synchronized (B) {} } });
Thread t2 = new Thread(() -> { synchronized (B) { synchronized (A) {} } });
t1.start();
t2.start();
t1.join(); //t2에서 Object B에 대해 Lock을 걸어둬서 대기 중
t2.join(); //t1에서 Object A에 대해 Lock을 걸어둬서 대기 중, 사실상 바로 위 t1.join(); 에서 이미 main 메소드 멈춤
}
}
위와 같이 두 스레드가 서로 필요한 동기화 객체에 Lock을 걸어두고 있어서 데드락에 빠짐
위 예시는 굳이 데드락을 만들기 위한 예시이며, 근본적으로 Lock을 거는 이유는, 공유하는 자원에 대한 동시성 문제를 방지하고자 함이다
💡해결 방법
락을 요청하는 순서를 정해준다
타임아웃을 설정하여 일정 시간 후 락을 해제한다
👎🏻 컨텍스트 스위칭 비용 (Context Switching Overhead)
“한 사람이 여러 개의 일을 번갈아가며 하면 집중력이 떨어진다.”
CPU는 한 번에 하나의 스레드만 실행할 수 있다
멀티 스레드를 사용할 때, CPU는 빠르게 스레드를 번갈아 실행하는데, 이때 각 스레드의 상태(레지스터, 스택 정보 등)를 저장하고 복원하는 작업(컨텍스트 스위칭)이 필요하다
컨텍스트 스위칭이 잦아지면 CPU가 실질적으로 연산하는 시간보다 문맥 전환하는 시간이 많아질 수 있기 때문에, 이 경우 성능이 오히려 저하될 수도 있다
컨텍스트 스위칭 문제에 대해 명확히 이해하기 위해서는, 실질적인 연산을 담당하는 CPU를 이해해야 한다
📌 예시
너무 많은 스레드를 생성하면 오히려 성능이 떨어지는 경우
웹 서버에서 트래픽이 많아질 때, 스레드가 과부하 상태에 빠지는 현상
💡 해결 방법
스레드 풀(Thread Pool) 사용 → 불필요한 스레드 생성을 줄이고 재사용
최적의 스레드 개수를 유지 → CPU 코어 수에 맞게 적절한 개수의 스레드 운영
🤞🏻 스레드 장단점 정리
장점
단점
✅ 병렬 처리를 통해 성능 향상 가능
❌ 동기화(동시성) 문제로 인해 데이터 충돌 가능
✅ 동일한 메모리 공간을 공유하여 자원 활용 효율적
❌ 데드락(교착 상태) 발생 가능
✅ 컨텍스트 스위칭 비용이 프로세스보다 적음
❌ 컨텍스트 스위칭 비용이 누적되면 성능 저하
📌 결론:
멀티 스레드는성능 최적화에 강력한 도구지만, 무분별하게 사용하면 오히려 성능 저하를 초래할 수 있다
따라서적절한 동기화 기법과 최적의 스레드 개수 조절이 중요하다
🏗 JVM에서의 스레드
1️⃣ JVM의 기본 스레드 구조
"공장 속 작업자들"
JVM은 하나의 공장이고, 이 공장에서 여러 작업자(스레드)가 동시에 일을 한다고 생각하면 된다
이 공장에는 기본적으로 운영에 필요한 필수 작업자들(시스템 스레드)과 사용자가 직접 고용한 일반 작업자들(애플리케이션 스레드)이 있다
메인 스레드 (Main Thread) 🏗️ → 프로그램이 실행될 때 자동으로 생성되는 스레드로, 메인 작업자가 업무를 시작하는 역할