도서 정리/자바의 정석

[자바의 정석] CH13 쓰레드 (1)

다아빈 2025. 5. 5. 20:48
728x90
반응형

자바의 정석 책을 공부하고 정리한 글입니다!

혹시라도 틀린 부분이 있다면 친절하게 알려주세요.

 

감사합니다!


CH13. 쓰레드(Thread) (1)

프로세스(process) 실행 중인 프로그램으로 데이터와 메모리 등의 자원과 쓰레드로 구성되어 있습니다.

쓰레드란 프로세스의 자원을 이용하여 실제로 작업을 수행하는 것을 말합니다. 따라서 모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재하며, 둘 이상의 쓰레드를 가진 프로세스 멀티쓰레드 프로레스라고 합니다.

 

프로세스를 공장으로 표현했을 때, 쓰레드는 일꾼이라 할 수 있습니다.

  • 싱글 쓰레드: 자원 + 일꾼
  • 멀티 쓰레드: 자원 + 일꾼 + 일꾼 + ... (여러 명의 일꾼)

즉, 멀티쓰레딩이란 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것을 말하며, 장점은 다음과 같습니다:

  • CPU의 사용률을 향상시킨다.
  • 자원을 보다 효율적으로 사용할 수 있다.
  • 사용자에 대한 응답성이 향상된다.
  • 작업이 분리되어 코드가 간결해진다.

예를 들어, 저희가 파일을 다운 받으면서 메신저를 보낼 수 있는 것도 멀티쓰레드로 작성되어 있기 때문입니다. 만약 싱글쓰레도로 작성되어 있었다면 파일을 다운 받는 동안에 다른 일을 할 수 없었을 것입니다.

 

하지만 멀티쓰레드는 여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업하기 때문에 동기화나 교착상태와 같은 문제가 발생할 수 있습니다.

더보기
더보기

교착상태란 두 쓰레드가 자원을 점유한 상태에서 서로 상대편이 점유한 자원을 사용하려고 기다리느라 진행이 멈춰있는 상태를 말합니다.

쓰레드는 두 가지 방법으로 구현할 수 있습니다.

  1. Thread 클래스를 상속하는 방법
  2. Runnable 인터페이스 구현하는 방법
// 1. Thread클래스 상속
class MyThread1 extends Thread {
    public void run() { /* 작업내용 */ } // Thread클래스의 run() 오버라이딩
}

// 2. Runnable인터페이스 구현
class MyThread2 implements Runnable {
    public void run() { /* 작업내용 */ } // Runnable인터페이스의 run() 구현
}

쓰레드를 생성할 때 어느 방법을 선택해도 상관없지만, Thread클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에 Runnable인터페이스를 구현하는 방법이 일반적입니다. Runnable인터페이스를 구현하는 방법은 재사용성이 높고 코드의 일관성을 유지할 수 있기 때문에 보다 객체지향적인 방법이라 할 수 있습니다.

 

인스턴스 생성방법은 다음과 같습니다.

// Thread클래스를 상속받은 경우
MyThread1 t1 = new MyThread1();

// Runnable인터페이스를 구현한 경우
Thread t2 = new Thread(new MyThread2());

Runnable인터페이스를 구현한 경우, Runnable인터페이스를 구현한 클래스의 인스턴스를 먼저 생성한 다음, 이 인스턴스를 Thread클래스의 생성자의 매개변수로 제공해야합니다.

 

그리고 start()를 호출하여 다음과 같으 쓰레드를 실행시킵니다.

t1.start();
t2.start();

start()가 호출되면 쓰레드는 실행 대기 상태가 됩니다. 이때 운영체제의 스케줄러가 해당 쓰레드에 CPU 를 할당해주면 run()메서드가 실행됩니다. 만약 다른 쓰레드가 없거나 CPU 자원이 여유로울 경우에는 즉시 실행될 수 있지만, 이는 스케줄러의 판단에 따라 달라집니다.

 

또한, 하나의 쓰레드에 대해 start()는 한 번만 호출이 가능합니다. 즉, 한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다는 것이죠. 만약 start()를 두 번 이상 호출하면 실행시 IllegalThreadStateException이 발생합니다.

start()와 run()

쓰레드를 실행시킬 때는 run()이 아닌 start()를 호출해서 실행시킵니다.

main메서드에서 run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 클래스에 선언된 메서드에 호출하는 것 뿐입니다.

반면, start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택을 생성한 다음, run()을 호출하여 호출스택에 run()이 첫 번째로 올라가게 합니다.

 

쓰레드의 생성과 실행흐름은 다음과 같습니다:

  1. main쓰레드에서 start()를 호출한다. (즉, main 쓰레드의 호출 스택에 start()가 올라가는 형태)
  2. JVM은 OS에 새로운 쓰레드의 생성을 요청한다.
  3. OS는 새로운 쓰레드를 생성하고, 새로운 쓰레드에 대한 호출 스택을 초기화 한다: 메모리 공간과 호출 스택이 새로 생성
  4. 새롭게 생성된 쓰레드는 run() 메서드를 자동으로 호출하여 실행한.
  5. run() 메서드가 끝나면 호출 스택이 비워지고 쓰레드가 종료된다.

스케줄러는 실행 대기 중인 쓰레드들의 우선순위를 고려하여 실행순서와 실행시간을 결정합니다. 각 쓰레드들은 작성된 스케줄에 따라 자신의 순서가 되면 지정된 시간동안 작업을 수행합니다.

 

이때 주어진 시간동안 작업을 마치지 못한 쓰레드는 다시 자신의 차례가 돌아올 때까지 대기상태로 있게 되며, 작업을 마친 쓰레드는 호출스택이 모두 비워지면서 이 쓰레드가 사용하던 호출스택은 사라지게 됩니다.

더보기
더보기

main쓰레드는 main메서드의 작업을 수행하는 쓰레드입니다. 프로그램이 실행하면 기본적으로 하나의 쓰레드를 생성하고, 그 쓰레드가 main메서드를 호출해 작업이 수행되도록 합니다. 지금까지는 main메서드가 수행을 마치면 프로그램이 종료되었으나, main메서드가 수행을 마쳤다하더라도 다른 쓰레드가 작업을 마치지 않은 상태라면 프로그램은 종료되지 않습니다.

싱글쓰레드와 멀티쓰레드

두 개의 작업을 하나의 쓰레드로 처리하는 경우와 두 개로 처리하는 경우를 가정해보겠습니다.

  • 하나로 처리하는 경우: 하나의 작업이 끝난 후에 다른 작업을 시작
  • 두 개로 처리하는 경우: 짧은 시간 동안 2개의 쓰레드를 번갈아 가며 작업을 수행 -> 동시에 두 작업이 처리되는 것과 같이 느낌

두 개의 쓰레드로 두 개의 작업을 처리하는 수행시간은 하나의 쓰레드로 두 개의 작업을 처리하는 수행 시간 거의 같거나 오히려 더 오래 걸릴 수 있는데, 쓰레드 간의 문맥 전환(Context Switching) 때문입니다.
문맥 전환 시에는 현재 수행 중인 작업의 상태(예: 프로그램 카운터 등)를 저장하고, 다음 스레드의 상태를 복원하는 과정이 필요하기 때문에 추가적인 오버헤드가 발생하게 됩니다.

 

따라서 싱글코어에서 단순히 CPU만 사용하는 계산작업이라면 멀티쓰레드보다 싱글 쓰레드로 프로그래밍하는 것이 더 효율적입니다.

더보기
더보기
  • 싱글코어: 하나의 코어만 가진 CPU로 한 번에 하나의 작업만 처리 가능 
  • 멀티코어: 여러 개의 코어를 가진 CPU로 코어마다 독립적으로 작업을 처리할 수 있음 -> 동시에 여러 작업 수행 가능

두 쓰레드가 서로 다른 자원을 사용하는 작업의 경우 싱글 쓰레드 프로세스보다 멀티 쓰레드 프로세스가 더 효율적입니다.

예를 들어, 파일을 다운받는 작업과 메세지를 보내는 작업을 하고 있다고 가정해보겠습니다.

  • 쓰레드 A: 파일을 다운받는 작업
  • 쓰레드 B: 메세지를 보내는 작업

이때 두 작업은 서로 영향을 주지 않는 독립적인 자원을 사용합니다. 따라서 하나의 쓰레드로 두 작업을 순차 처리하는 것보다 두 쓰레드가 동시에 각각의 작업을 처리하는 것이 전체 시간을 단축할 수 있으며 더 효율적입니다.

쓰레드의 우선순위

쓰레드는 우선순위(priority)라는 멤버변수를 가지고 있는데, 이 우선순위 값에 따라 쓰레드가 얻는 실행시간이 달라집니다. 쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수도 있습니다.

 

먼저, 쓰레드의 우선순위와 관련된 메서드와 상수는 다음과 같습니다

void setPriority(int newPriority) // 우선순위 설정
int getPriority() // 우선순위 반환

public static final int MAX_PRIORITY = 10 // 최대우선순위
public static final int MIN_PRIORITY = 1 // 최소 우선순위
public static final int NORM_PRIORITY = 5 // 보통 우선순위

쓰레드가 가질 수 있는 우선순위의 범위는 1~10이며 숫자가 높을수록 우선순위가 높습니다.

또한 쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받는다는 것입니다. main메서드를 수행하는 쓰레드는 우선순위가 5이므로 main메서드 내에서 생성하는 쓰레드의 우선순위는 자동적으로 5가 됩니다. 하지만 이는 setPriority()메서드로 변경할 수 있습니다.

 

우선순위가 같은 경우 각 쓰레드에게 거의 같은 양의 실행시간이 주어지지만, 우선순위가 다르다면 우선순위가 높은 쓰레드에게 상대적으로 더 많은 양의 실행시간이 주어집니다. 결과적으로 우선순위가 더 높은 쓰레드가 낮은 쓰레드에 비해 더 빨리 완료될 수 있습니다.

 

728x90
반응형