[Java] 멀티스레드의 기초 이해

2026. 1. 22. 00:06·Java

 

 

멀티스레드를 이해하기 위한 운영체제 기초 개념 정리

멀티스레드를 제대로 이해하기 위해서는 운영체제의 기본적인 실행 구조를 알아야 합니다. 프로그램 실행이란 프로그램을 구성하는 코드가 CPU에서 순차적으로 계산되는 과정을 의미합니다. CPU 코어가 1개라면 하나의 작업만 실행할 수 있습니다.

 

예를 들어 음악을 듣는 프로그램 A, 웹서핑을 하려는 프로그램 B 2개를 동시 실행을 하고 싶지만, 단일 프로그램 실행이 되기 때문에 프로그램 A 음악을 다 들어야 웹서핑을 하려는 프로그램 B가 실행이 됩니다. 실제 초창기의 컴퓨터는 이러한 방식이었으며 이를 해결하기 위해 한 개의 CPU 코어로 여러 프로그램을 동시에 실행하는 멀티 태스킹 기술이 등장했습니다.

 

 

멀티테스킹

애니메이션이나 영상이 초당 30 ~ 60장의 이미지를 빠르게 보여주어, 사람이 연속적인 영상으로 인식하는 것처럼, CPU도 여러 프로그램의 코드를 아주 짧은 시간 단위로 번갈아 수행한다. 그렇다면 두 프로그램이 동시에 실행되는 것처럼 느껴질 것이다.

 

- 프로그램 A 실행(약 0.01초)

- 프로그램 A 중단

- 프로그램 B 실행(약 0.01초)

- 프로그램 B 중단

- 프로그램 A 실행(약 0.01초)

 

이러한 동작을 반복 실행한다. 이렇게 실행하면 사용자 입장에서는 여러 프로그램이 동시에 실행되는 것처럼 느껴진다. 프로그램의 실행 시간을 분할해서 마치 동시에 실행하는 것 처럼 하는 기법을 시분할(Time Sharing, 시간 공유) 기법이라고 한다.

 

참고로 CPU에 어떤 프로그램이 얼마나 실행될지는 운영체제가 결정하는데 이를 스케줄링이라고 한다. 스케줄링은 단순히 시간으로 작업을 분할하지 않고  다양한 조건들을 고려하여 CPU 자원을 최적화해준다.

 

 

멀티프로세싱

CPU 코어가 여러 개라면 동시에 여러 개의 프로그램을 처리할 수 있으니, 더욱 원활한 데이터 처리가 가능합니다. 보다 편한 이해를 위해, 프로그램 A, B, C의 순서대로 각 CPU 코어에 넣었지만, 순서는 스케줄링으로 인하여 계속 변경됩니다.

 

멀티태스킹 vs 멀티프로세싱

구분 멀티테스킹 멀티프로세싱
관점 운영체제(소프트웨어) 하드웨어
핵심 CPU 시간을 나눠 사용 다수의 CPU 코어
특징 동시에 실행되는 것처럼 보임 실제로 동시에 실행

 

 

프로세스 & 스레드

프로그램은 실행하기 전까지 단순한 파일이다. 프로그램을 실행하면, 운영체제는 프로세스를 생성한다. 프로세스란 실행 중인 프로그램의 인스턴스이다. 각 프로세스는 독립적인 메모리 공간을 갖고 있으며, 운영체제에서 별도의 작업 단위로 분리해서 관리합니다.

 

각 프로세스는 별도의 메모리 공간을 갖고 있기에 서로 간섭하지 않으며, 서로의 메모리에 직접 접근 불가능 합니다. 그래서 프로세스는 서로 격리되어 있어 하나의 프로세스가 충돌해도 다른 프로세스에 영향을 끼치지 않습니다.

 

모든 프로세스는 최소 하나 이상의 스레드를 가집니다. 스레드는 프로세스 내에서 실행되는 작업의 단위이며, 한 프로세스 내에 여러 스레드 존재 가능하며 스레드들은 프로세스의 메모리 공간을 공유하며, 각 스레드는 개별적으로 자신의 Stack 영역을 갖고 있습니다.

 

프로세스의 메모리 구조
코드 영역 실행할 프로그램의 코드 저장
데이터 영역(기타) 전역 변수 및 static 변수 저장
힙 (Heap) 영역 동적으로 할당 영역
스택(Stack) 영역 메서드 호출 시 생성되는 지역 변수와 반환 주소가 저장되는 영역(스레드에 포함)

 

 

그렇다면 프로그램은 무엇에 의해 실행이 되는 걸까요? CPU는 프로세스가 아니라, 스레드를 실행합니다. 지금까지 저희가 작성했던 코드가 순차적으로 실행된 이유는 자바의 기본 제공 스레드가 CPU를 사용해 코드를 읽고 실행했기 때문입니다.

 

 

멀티스레드

저희는 일상에서도, 여러 작업을 동시에 처리하는 경우가 많습니다. 예를 들어 유튜브 뮤직으로 노래를 들으며 가사를 본다던지, 넷플릭스로 영상을 재생하며, 자막을 보는 등 여러 작업을 동시에 하기에 멀티스레딩이 필요합니다. 운영체제적인 관점에서 본다면 다음과 같습니다.

 

유튜브 뮤직 - 프로세스 A

- 스레드 1 : 노래 재생

- 스레드 2 : 가사 표출

 

넷플릭스 - 프로세스 B

- 스레드 1 : 영상 재생

- 스레드 2: 자막 표출

 

 

스케줄링

스케줄링이란, 앞서 말했던 것처럼 운영 체제가 여러 스레드 중 어떤 스레드를 언제, 얼마나 CPU에서 실행할지 결정하는 과정이다. 운영체제는 실행 대기 중인 스레드들을 스케줄링 큐(Scheduling Queue)라는 대기 공간에 넣어 관리한다.

 

단일 스케줄링의 예시를 보겠습니다.

 

 

- 스케줄링 큐에서 스레드 A1, B1, B2를 다 넣고 CPU 코어에 들어가기 전까지 대기

- 스레드 A1를 CPU 코어에서 실행

- 스레A1 잠시 멈추고 스케줄링 큐에 다시 반환 후 운영체제를 비움

- 스레드 B1을 스케줄링 큐에서 꺼내 CPU 코어에서 실행

이후 계속 반복

 

멀티 코어 스케줄링

CPU코어가 2개 이상이면 한 번에 더 많은 스레드를 물리적으로 진짜 동시에 실행할 수 있으니, 더욱 원활한 데이터 처리가 가능합니다.

 

콘텍스트 스위칭(Context Switching)

하스케줄링을 통한 멀티태스킹 방식은 매우 유용하지만, 항상 효율적이지 않습니다. 그 이유는 바로 콘텍스트 스위치 비용이 발생하기 때문입니다. 컨텍스트 스위칭이란 CPU가 실행 중인 스레드를 바꾸기 위해, 현재 스레드의 실행 상태를 저장하고 다음 스레드의 상태를 복원하는 과정입니다.

 

간단하게 게임으로 예시를 들자면 2개의 게임 데이터를 각각 계속 저장하고 로드하면서, 게임을 하는 것입니다. 이 데이터를 저장하고 로드할 때는 시간이 소요되므로, 이렇게 소요되는 시간을 콘텍스트 스위칭이라고 합니다. 즉 스케줄링을 통한 멀티태스킹은 스레드 A, B, C를 스위칭하면서 실행할 때 저장되고 복원하는 시간이 소요됩니다. 콘텍스트 스위칭에 걸리는 시간은 아주 짧지만, 스레드가 매우 많다면 이 비용은 매우 커질 수 있습니다.

 

 

CPU 바운드 작업 vs I/O 바운드 작업

스레드의 개수는 무조건 많다고 좋은 것이 아니며, CPU 바운드 작업이 많은지, I/O 바운드 작업이 많은지 작업의 성격에 따라 적절한 스레드 수를 다르게 설정해야 합니다.

 

CPU 바운드 작업이란 CPU 연산이 중심(수학 계산, 이미지 처리, 암호화 등)을 뜻하며, 스레드 수와 CPU 코어 수를 비슷하게 하는 것이 효율적입니다. 너무 많은 스레드는 불필요한 콘텍스트 스위칭 비용을 증가시켜 결과적으로 연산 시간 + 컨텍스트 스위칭 비용이 되기 때문에 오히려 성능이 감소할 수 있습니다.

 

I/O 바운드 작업이란 입출력 대기 시간이 많은 작업이 중심(파일 읽기, 네트워크 통신, DB 조회)을 뜻하며, 대부분의 백엔드 개발자분들은 I/O 바운드 작업이 더 많다고 합니다. 입출력 대기 시간이 많아, CPU는 대부분 대기 상태이고 스레드가 대기하는 동안 다른 스레드를 실행할 수 있기에 CPU보다 훨씬 많은 스레드가 필요하다고 합니다.

 

하지만 두 내용 모두 일반적인 내용이니, 개별적인 성능 확인이 필요합니다.

 

 

 

스레드 생성

자바는 스레드도 객체로 다루며, 스레드를 생성하는 방 법은 두 가지 방법이 있습니다. 하나의 방법은 Thread 클래스를 상속받는 방법과 Runnable 인터페이스를 구현하는 방법이 있습니다

 

<Thread 클래스 상속>

TestThread라는 클래스를 Thread를 상속받아 생성하고, run()은 오버라이딩하여 출력해 보겠습니다.

 

여기서 오버라이딩한 run()을 호출하는 게 아니라 start()을 호출했고, run()의 코드가 정상 실행이 됐습니다. 그 이유는 뭘까요? 

run()을 직접 실행하면 Thread-0 스레드가 실행하는 것이 아니라 메인 스레드가 실행하는 것임. 즉 스레드를 한 개 더 만들어 병렬로 실행하는 게 아니라, main이 순차적으로 실행하는 것으로, 자바 기본 main스레드에서 run() 메서드를 호출해 처리한 것뿐 스레드를 생성하지 않은 것입니다.

 

프로세스가 작동하려면 스레드가 최소한 하나는 있어야 하며, 자바는 실행 시점에 main이라는 이름의 스레드를 만들고 프로그램의 시작점인 main() 메서드를 실행 한다. 이는 main 클래스 당 스레드가 1개가 아니라, 자바 프로그램(JVM 프로세스) 하나당 기본 스레드가 1개 생성됨을 의미합니다.

 

 

 

자바의 메모리 구조

잠시 자바의 메모리 구조를 다시 생각해보겠습니다. 자바의 메모리 구조는 크게 메서드 영역, 스택 영역, 힙 영역으로 나뉘며 각 영역마다 관리하는 내용은 상이합니다. 또한 프로세스가 작동하려면 스레드가 최소한 하나는 있어야 하며, 자바는 실행 시점에 main()이라는 이름의 스레드를 생성하고, 프로그램의 시작점인 main()메서드를 실행합니다.

 

 

메서드 영역(Method Area) : 프로그램을 실행하는데 필요한 공통 데이터 관리. 프로그램의 모든 영역에서 공유합니다.

클래스 정보 : 클래스의 실행 코드,. 필드, 메서드와 생성자 코드 등 모든 실행 코드

static 영역 : static 변수들

런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수 

 

스택영역(Stack Area) : 자바 실행 시 하나의 실행 스택이 생성되며 각 스택 프레임은 지역 변수, 중간 연산결과, 메서드 호출 정보 등을 포함합니다.

스택 프레임 : 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임이고, 메서드를 호출할 때마다 하나의 스택 프레임이 쌓이고 메서드가 종류 되면 해당 스택 프레임이 제거됩니다. 더 정확히는 각 스레드별로 하나의 실행 스택이 생성되고, 스레드 수 만큼 스택이 생성 됩니다. 지금은 스레드를 1개만 사용하므로 스택도 1개 입니다.

 

힙 영역(Heap Area) : 객체와 배열이 생성되는 영역. GC가 이루어지는 주요 영역이며, 더 이상 참조되지 않는 객체는 GC에 의해 제거됩니다. 

 

 

Thread 객체를 생성한 다음 start() 메서드를 호출하면 자바는 해당 스레드를 위한 별도의 스택 공간을 할당함. new Thread()는 단순히 객체를 생성하는 것뿐이며, start() 호출 메서드를 호출해야만 실제 스레드가 생성되고 실행이 시작됩니다. 또한 스레드에 이름을 지정하지 않으면 스레드에 Thrtead-0, Thrtead-1과 같은 이름을 부여하고, Thread-0 스레드는 run() 메서드의 스택 프레임을 스택에 올리면서 run() 메서드를 시작합니다.

 

메서드를 실행하면 스택 위에 스택 프레임이 쌓이며, main 스레드는 main() 메서드의 스택 프레임을 스택에 올리면서 시작하고, Thread-0 스레드는 run*(메서드의 스택 프레임을 스택에 올리면서 시작합니다.

 

멀티스레드(2)

main 스레드가 Thread 인스턴스를 생성하고, start()를 호출하면 Thread-0 스레드 생성 되면서, run 메서드를 실행합니다. 이때 중요한 점은 Thread-0의 새로운 스레드가 run()을 실행한 것으로, main 스레드는 단지 "이 작업을 새 스레드에게 맡겨라" 지시만 하고 즉시 다음 코드를 계속 실행합니다. 즉 main 스레드는 run()이 끝날 때까지 기다리지 않고, 즉시 다음 코드를 계속 실행합니다. 따라서 main 스레드와 Thread-0의 스레드는 서로 독립적으로 동시에 실행하게 됩니다.

 

멀티스레드는 스레드 간 실행 순서는 보장되지 않으며, 어떤 스레드가 먼저 실행될지, 언제 끝날지 알 수 없습니다. 각 스레드에 우선순위라는 것이 있고, 우선순위를 설정할 수 있지만 이 또한 절대적인 영향을 끼치지 않고 실행 순서에 대한 확률을 높일 뿐, 순서를 보장하지 않습니다. 이는 우선순위가 7인 스레드 A가 우선순위 2인 스레드 B보다 실행될 수 있음을 뜻합니다. 우선순위는 1 ~ 10까지 있으며 기본 우선순위는 5로 setPriority(int)를 통해 설정할 수 있습니다.

 

<Runnable 인터페이스 방식>

Runnable 인터페이스를 구현한 MyTask를 만들고, 메인 클래스에 Thread에 객체를 생성해 실행하면 됩니다. Runnable 인터페이스는 중첩 클래스, 람다식 등 다양한 방법으로 구현이 가능합니다. 이때 MyTask는 Runnable을 구현 클래스일 뿐 Thread 클래스는 아닙니다.

 

간단히 말하자면 Thread : 작업자, start() : 작업 시작 버튼, Runnable : 작업 도구라고 생각할 수 있습니다. 실무에서는 Runnable 인터페이스를 구현하는 방식을 많이 사용하고 그 이유는 자바는 단일 상속만 가능하기에, 다른 클래스를 상속받아도 스레드를 사용할 수 있기 때문입니다. 또한 스레드와 실행할 작업을 분리하여 역할이 명확해져, 코드의 가독성을 높이고 유지보수성이 향상됩니다. 다만 Runnable 객체를 생성하고 이를 Thread 전달하는 과정이 추가되어 코드가 조금은 복잡해질 수 있습니다.

 

데몬스레드

스레드는 사용자(USER) 스레드와 데몬(daemon) 스레드 2가지 종류로 구분이 가능합니다.

 

<사용자(user) 스레드(non-daemon 스레드) >

프로그램의 주요 작업을 수행하며, 작업이 완료될 때까지 실행합니다. 모든 사용자의 스레드가 종료되면, JVM도 종료됩니다.

 

<데몬 스레드(daemon 스레드) >

백그라운드에서 보조적인 작업 수행하며, 모든 사용자의 스레드가 종료되면 데몬 스레드의 작업 진행률과 상관없이 데몬 스레드는 자동으로 종료됩니다. 사용자에게 직접적으로 보이지 않고, 시스템의 백그라운드에서 실행하는 것을 데몬 스레드, 데몬 프로세스라고 합니다. setDaemon(boolean)을 통해 데몬스레드를 설정할 수 있으며, 기본 값은 false(사용자 스레드)입니다. 저희가 흔히 말하는 가비지콜렉터(Garbage Collector)도 대표적인 데몬 스레드입니다.

'Java' 카테고리의 다른 글

[Java] 컬렉션 프레임워크 - Set(HashSet, TreeSet)의 이해  (1) 2026.03.03
[Java] 컬렉션 프레임워크 - List(ArrayList, LinkedList)의 이해  (0) 2026.02.23
[Java] 시간복잡도와 빅 오(Big-O) 표기의 이해  (1) 2026.02.18
[Java] I/O스트림의 이해와 활용  (1) 2026.02.09
[Java] 멀티스레드의 이해 (스레드의 상태와 동기화 중요성 )  (1) 2026.01.30
'Java' 카테고리의 다른 글
  • [Java] 컬렉션 프레임워크 - List(ArrayList, LinkedList)의 이해
  • [Java] 시간복잡도와 빅 오(Big-O) 표기의 이해
  • [Java] I/O스트림의 이해와 활용
  • [Java] 멀티스레드의 이해 (스레드의 상태와 동기화 중요성 )
mins0on
mins0on
비전공자의 백엔드 개발자 공부 기록 일지입니다.
  • mins0on
    꾸준함의 가치
    mins0on
  • 전체
    오늘
    어제
    • 분류 전체보기 (65) N
      • Java (7)
      • Spring (9)
      • DataBase (1)
      • Algorithm (1)
      • Network (6)
      • 운영체제 (2)
      • 코드 분석 (26)
      • Trouble Shooting (4) N
      • Project (1)
      • Migration (3)
      • 기타 (1)
      • 개념 정리 (3)
      • Coding Test (1)
        • Baekjoon (1)
  • hELLO· Designed By정상우.v4.10.6
mins0on
[Java] 멀티스레드의 기초 이해
상단으로

티스토리툴바