왜 스레드가 필요한가? 컴퓨터의 기본 구성 요소인 입력/출력 장치, 계산기, 저장 장치, 제어 장치 간 성능 차이를 해결하기 위해 하드웨어와 소프트웨어에서 다양한 기술이 도입되었습니다. CPU는 캐시를 추가해 메모리 접근 속도를 조절하고, 운영체제는 프로세스와 스레드를 통해 CPU 자원을 분할 처리하며, 컴파일러는 명령어 실행 순서를 최적화하여 성능 향상을 추구합니다. 이러한 기술들은 병렬 처리 효율성을 높이고 시스템 전체 동작의 안정성을 보장합니다.
스레드 불안전 예시
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadSafetyDemo {
private int counter = 0;
public void increment() {
counter++;
}
public int getValue() {
return counter;
}
public static void main(String[] args) throws InterruptedException {
final int THREAD_COUNT = 1000;
ThreadSafetyDemo demo = new ThreadSafetyDemo();
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < THREAD_COUNT; i++) {
executor.execute(() -> {
demo.increment();
latch.countDown();
});
}
latch.await();
executor.shutdown();
System.out.println(demo.getValue());
}
}
문제 원인 분석 다수의 스레드가 공유 변수에 접근할 때 발생하는 비동기 문제의 근본 원인은 세 가지입니다:
- 가시성(Visibility) 스레드 A가 변수를 변경하면 스크래치 B는 즉시 해당 값을 인식해야 합니다. 그러나 CPU 캐시 동기화 미비로 인해 다음과 같은 문제가 발생할 수 있습니다:
// 스레드 A
int value = 0;
value = 10;
// 스레드 B
int result = value;
스레드 A가 메모리에 값 업데이트를 하지 않아 스레드 B는 여전히 초기 값 0을 참조하게 됩니다.
-
원자성(Atomicity) 예를 들어 은행 이체 시 계좌 A에서 1000원을 감소시키고 계좌 B에 1000원을 증가시키는 두 작업이 모두 완료되어야 합니다. 만약 이 과정에서 중단이 발생하면 데이터 일관성이 깨질 수 있습니다.
-
순서 보존(Ordering) 다음 코드는 실행 순서가 보장되지 않을 수 있습니다:
int var1 = 0;
boolean flagVar = false;
var1 = 1; // 명령어 1
flagVar = true; // 명령어 2
컴파일러나 프로세서의 명령어 재배치로 인해 실제 실행 순서가 달라질 수 있습니다.
핵심 개념 정리 병렬(Parallel) vs 동시성(Concurrency)
- 병렬: 단일 CPU 환경에서 여러 프로세스가 동시에 실행
- 동시성: 한 번에 하나의 프로세스만 실행
프로세스 vs 스레드 vs 코루틴
- 프로세스: 독립적인 메모리 공간을 갖는 자원 할당 단위
- 스레드: 프로세스 내부의 실행 단위, 공유된 자원 사용
- 코루틴: 사용자 영역에서 관리되는 경량 스레드