이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › Java › 스레드와 동시성
스레드 수와 컨텍스트 스위칭
스레드를 늘린다고 계속 빨라지지는 않는다
CPU 코어 하나는 한순간에 스레드 하나만 실행합니다. 실행하려는 스레드가 코어보다 많으면 운영체제가 짧은 시간씩 번갈아 실행시킵니다(컨텍스트 스위칭). 그러면 처리량은 더 늘지 않고 작업 하나가 끝나는 시간만 길어집니다. 작업이 계산 위주인지 기다림 위주인지에 따라 적당한 스레드 수가 완전히 달라집니다.
일부만 실행으로 확인 · JDK 21 실측과 방향이 같음(수치는 모형)
이 실습에 나오는 개념
- 프로세스와 스레드
- 프로세스는 실행 중인 프로그램으로, 운영체제로부터 독립된 메모리 공간을 할당받는다. 스레드는 프로세스 안에서 실제로 작업을 수행하는 실행 흐름으로, 같은 프로세스의 스레드들은 메모리(힙, 코드, 데이터)를 공유하고 스택만 따로 가진다. 그래서 스레드 간에는 데이터를 주고받기 쉽지만 동시에 같은 데이터를 건드리는 문제가 생긴다.
- 컨텍스트 스위칭
- CPU가 실행하던 스레드(또는 프로세스)를 멈추고 다른 것으로 바꿔 실행하는 과정이다. 하던 작업의 상태를 저장하고 다음 작업의 상태를 불러와야 하며, 이 전환 자체에 시간이 든다. 스레드가 CPU 코어 수보다 지나치게 많으면 실제 작업보다 전환에 드는 시간이 늘어난다.
- 스레드 풀
- 스레드를 필요할 때마다 만들고 없애는 대신 일정 수의 스레드를 유지하며 재사용하는 구조다. 스레드 생성 비용을 줄이고, 동시에 실행되는 스레드 수를 제한해 과도한 메모리 사용과 컨텍스트 스위칭을 막는다. 처리할 작업은 큐에 쌓이고 빈 스레드가 가져가 실행한다. 톰캣의 요청 처리, DB 커넥션 풀도 같은 발상이다.
- 블로킹과 논블로킹
- 블로킹은 호출한 작업이 끝날 때까지 호출한 스레드가 멈춰서 다른 일을 하지 못하는 것이다. 논블로킹은 작업이 끝나지 않았어도 호출이 바로 반환되어 스레드가 다른 일을 할 수 있는 것이다. 일반적인 JDBC 호출은 DB 응답이 올 때까지 스레드가 멈추는 블로킹 호출이다.
이렇게 바꿔 보세요
- 처음 설정으로 — 코어 4개에 계산 작업 스레드 16개. 처리량은 4개일 때와 같고, 작업 하나가 끝나는 시간만 4배가 됩니다.
- 스레드를 코어 수에 맞추면 — 처리량은 같고 작업마다 기다림 없이 끝납니다.
- 기다림 위주 작업, 스레드 4개 — 스레드가 대부분 기다리고 있어 CPU가 놉니다.
- 기다림 위주 작업, 스레드 40개 — 기다리는 동안 다른 스레드가 CPU를 써서 처리량이 10배가 됩니다.
- 그보다 더 늘리면 — 80개. CPU가 이미 꽉 차서 처리량은 그대로입니다.
처음 설정으로 실행하면
- 코어 4개, 스레드 16개입니다. 각 스레드는 쉬지 않고 계산합니다.
- 어느 순간에 CPU를 쓰려는 스레드는 평균 16개입니다(16개 × 100%). 코어는 4개이므로 코어가 모자랍니다. 운영체제가 스레드들을 짧은 시간씩 번갈아 코어에 올립니다.
- 스레드를 교대할 때마다 운영체제는 하던 스레드의 상태를 저장하고 다음 스레드의 상태를 불러옵니다. 코어 하나를 스레드 약 4개가 나눠 쓰므로, 각 스레드는 자기 차례가 아닌 동안 멈춰 있습니다. 전체가 하는 일의 양은 코어 4개 분량 그대로이고, 각자의 작업이 끝나는 시간만 길어집니다.
- 결과: CPU 사용률 100%, 초당 40건 처리, 작업 하나는 0.4초 만에 끝납니다(혼자 실행하면 0.1초). 스레드를 4개로 맞추면 초당 40건, 작업 하나 0.1초입니다.
실행 결과
- 계산 위주 작업은 코어 수보다 스레드를 많이 둬도 처리량이 늘지 않습니다. 코어를 나눠 쓰느라 작업 하나하나가 늦게 끝날 뿐입니다. 스레드마다 메모리(스택)도 따로 쓰므로, 스레드 수는 코어 수 근처가 적당합니다.
- 웹 서버의 요청 처리 스레드가 코어 수보다 훨씬 많은(톰캣 기본 200개) 이유는 요청 처리의 대부분이 DB나 외부 응답을 기다리는 시간이기 때문입니다.
코드
int cores = Runtime.getRuntime().availableProcessors(); // 4
// 계산 위주: 코어 수만큼이면 충분
ExecutorService pool = Executors.newFixedThreadPool(16);
for (Task task : tasks) {
pool.submit(() -> resize(image));
}
이 모형의 전제
작업 하나가 CPU를 0.1초 쓴다고 가정한 모형입니다. 이 PC(논리 코어 12개)의 JDK 21에서 실제로 잰 결과와 방향이 같습니다. 계산 위주 작업은 스레드 1개 초당 102건, 6개 562건, 12개 768건, 24개 820건, 48개 826건으로 코어 수 부근에서 더 늘지 않았습니다. 기다림 위주 작업(계산 1ms + 대기 9ms)은 스레드 12개 1,104건, 48개 4,417건, 120개 7,871건, 240개 8,202건, 480개 8,144건으로 코어 수의 10배 부근까지 늘다가 멈췄습니다. 스레드가 코어보다 훨씬 많아도 처리량이 눈에 띄게 줄지는 않았습니다. 교대 자체의 비용은 이 측정에서 드러나지 않을 만큼 작았고, 그래서 모형에도 처리량 감소를 넣지 않았습니다. 실측에서 코어 수만큼 정확히 비례하지 않는 것은 논리 코어 12개가 물리 코어 6개를 나눠 쓰기 때문입니다. 적정 스레드 수의 식은 자바 동시성 분야에서 널리 인용되는 기준입니다.
앞 실습: 스레드 풀 · 다음 실습: 순차 호출과 병렬 호출