요즘 ‘페스타고’에서 동시성 문제를 해결하며 병렬 프로그래밍에 대한 관심이 커졌다.
자바에서 동시에 여러 작업을 수행하기 위해 사용되는 스레드(Thread)에 대해 알아보자.
프로세스와 스레드
운영체제가 없던 시절, 컴퓨터는 한 번에 프로그램 하나만 실행했다. 그리고 해당 프로그램은 컴퓨터 내 모든 자원을 직접 접근할 수 있었다.
운영체제가 등장하고, 여러 개의 프로그램을 각자의 프로세스 내에서 동시에 실행할 수 있게 되었다.
프로세스(process)는 각자가 서로 격리된 채로 독립적으로 실행하는 프로그램으로, 운영체제는 프로세스마다 메모리, 파일 핸들, 보안 권한 등의 자원을 할당한다.
프로세스의 등장으로 자원 활용, 공정성, 편의성을 얻을 수 있었다.
스레드의 등장으로, 한 프로세스 안에 여러 개의 프로그램 제어 흐름이 공존할 수 있게 되었다.
스레드(Thread)는 프로세스의 자원을 이용해 실제로 작업을 수행하는 역할을 한다.
스레드는 메모리, 파일 핸들과 같이 프로세스에 할당된 자원을 공유한다.
각 스레드는 각기 별도의 프로그램 카운터(PC), 스택, 지역 변수를 갖는다.
스레드 생성 방법
java의 스래드는 java.lang.Thread
클래스와 java.lang.Runnable
인터페이스를 통해 구현되며, JVM은 멀티스레드 환경을 지원한다.
Thread 클래스 상속
Thread 클래스를 직접 상속받아 새로운 하위 클래스를 정의한다.
run() 오버라이드해 스레드가 실행할 작업을 구현한다.
java는 다중 상속을 지원하지 않기 때문에, Thread 클래스 이외의 다른 클래스를 상속받을 수 없다는 단점이 존재한다.
class MyThread extends Thread {
@Override
public void run() {
/***/
}
}
MyThread t = new MyThread();
t.start(); // 스레드 시작
Runnable 인터페이스 구현
Runnable은 함수형 인터페이스로, 단일 추상 메서드인 run()을 포함하고 있다.
Runnable 구현체는 run() 메서드에 스레드가 실행할 작업을 구현한다.
Runnable 객체는 Thread 클래스의 생성자에 전달되어 스레드를 생성할 수 있다.
이는 재사용성이 높고 코드의 일관성을 유지할 수 있기 때문에 보다 객체지향적인 방법이다.
class MyRunnable implements Runnable {
@Override
public void run() {
/***/
}
}
Thread t = new Thread(new MyRunnable());
t.start(); // 스레드 시작
Thread 클래스를 상속받은 경우, 직접 Thread클래스 메서드를 직접 호출할 수 있다.
반면 Runnable 인터페이스를 구현한 경우, Thread클래스 메서드를 호출하려면 Thread.currentThread()를 호출하여 현재 실행 중인 스레드의 참조를 얻어야 한다.
class MyThread extends Thread {
@Override
public void run() {
System.out.println(getName());
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
스레드의 상태
스래드는 아래와 같은 상태들을 가진다.
- NEW: 스레드가 생성되었지만 아직 시작되지 않음.
- RUNNABLE: 실행 중 / 실행 대기 중
- BLOCKED: 모니터 락을 기다리는 중
- WAITING: 스레드 작업이 종료되지는 않았지만 실행가능하지 않은 일시정지 상태
- TIME_WAITING: 일시 정지 기간이 지정된 경우
- TERMINATED: 스레드 작업 종료 / 예외로 종료
스레드 실행 - start()
스레드의 실행은 Thread의 start() 메서드를 호출함으로써 시작된다.
start() 메서드를 호출함으로써 스레드가 실행 대기 상태(RUNNABLE)로 전환되며, 스레드 스케줄러에 의해 스레드의 실행이 관리된다.
주의할 점은 start() 메서드는 한 스레드당 단 한 번만 호출가능하며, 두 번 이상 호출 시 IllegalThreadStateException 예외가 발생한다.
따라서 한 번 실행이 종료된 스레드는 다시 실행할 수 없고, 새로운 스레드를 생성해 start()를 호출해야 한다.
또한, run() 메서드가 아닌 start() 메서드를 호출해야 한다.
만약 run() 메서드를 직접 호출하면 단순히 현재 스레드에서 메서드가 실행되고, 별도의 새로운 스레드가 생성되지 않는다.
호출 스택
스레드가 실행될 때, 각 스레드는 독립적인 호출 스택(call stack)을 가진다.
호출 스택은 스레드가 수행하는 메서드 호출을 관리하며, 현재 실행 중인 메서드는 호출 스택의 가장 위에 위치한다.
다른 스레드의 호출 스택과는 독립적이므로, 한 스레드에서 예외가 발생해도 다른 스레드에 영향을 주지 않는다.
스케줄러가 실행 대기 중인 스레드들의 우선순위를 고려해 실행순서, 실행시간을 결정한다.
각 스레드는 스케줄링에 따라 작업을 수행하고, 주어진 실행 시간 내에 작업을 마치지 못하면 다시 실행 대기 상태로 전환되어 다음 차례를 기다린다.
스레드는 독립적인 실행 흐름을 가지며, 여러 개의 스레드가 동시에 실행될 수 있어 병렬 처리와 동시성 작업을 가능하게 한다.
각종 메서드
Thread.sleep(long millis)
sleep() 메서드는 Thread 클래스의 스태틱 메서드로, 스레드를 주어진 시간 동안 일시 정지시킨다. (WAITING 상태로 전환한다)
주어진 시간이 지나거나, 다른 스레드에 의해 interrupt() 메서드가 호출되면 스레드가 깨어나고 RUNNABLE 상태로 전환된다.
WAITING 상태에서 interrupt() 메서드가 호출되면 InterruptedException이 발생하기 때문에 예외 처리가 필요하다.
try {
Thread.sleep(1000);
} catch(InterruptedException e){
e.printStackTrace();
}
interrupt 관련
interrupt()
interrupt() 메서드는 스레드에게 작업을 멈추라고 요청하고, 스레드의 인터럽트 상태를 true로 변경한다.
이는 스레드에게 작업을 중단하라는 신호를 보내는 것이며, 스레드 작업을 강제 종료하는 것은 아니다.
WAITING 상태인 스레드에 해당 메서드를 호출할 경우, InterruptedException이 발생하고 스레드는 RUNNABLE 상태로 바뀐다. (멈춰있는 스레드를 깨워서 실행가능한 상태로 만든다.)
interrupted()
interrupted() 메서드는 현재 스레드가 인터럽트 되었는지 확인하는 데 사용된다.
호출 시 현재 스레드의 인터럽트 플래그를 확인하고, 플래그를 false로 초기화한다.
yield()
yield() 메서드는 현재 실행 중인 스레드가 다음 차례의 스레드에게 실행을 양보하는 메서드이다. 호출한 스레드는 실행 대기 상태로 바뀐다.
yield() 메서드를 통해 스레드 간 실행 우선순위를 조절하고, 공정한 실행을 도모할 수 있다.
join()
join() 메서드는 현재 실행 중인 스레드가 대기하도록 하는 메서드이다. 이 메서드를 호출한 스레드는 지정된 스레드가 작업을 마칠 때까지 대기한다. 이를 통해 스레드의 실행 순서를 제어할 수 있다.
예를 들어, 스레드 A에서 스레드 B의 join() 메서드를 호출하면, 스레드 A는 스레드 B의 작업이 끝날 때까지 기다린다.
파라미터로 최대 대기 시간을 지정할 수도 있다. 이는 스레드 B의 작업이 지정된 시간 내에 완료되지 않을 경우, 스레드 A가 대기에서 풀려나게 하는 데 사용된다.
Thread thread = new Thread(() -> {
/***/
});
thread.start(); // 스레드 시작
try {
thread.join(); // 현재 스레드가 thread 스레드의 작업이 끝날 때까지 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
참고 자료
자바 병렬 프로그래밍
자바의 정석 (3rd edition)
'프로그래밍' 카테고리의 다른 글
Spring Event란? (0) | 2023.09.17 |
---|---|
[Java] synchronized란? (1) | 2023.09.13 |
Facade 객체를 활용해 트랜잭션에서 외부 API 통신 분리하기 (4) | 2023.09.11 |
[Java] I/O Stream (3) | 2023.09.02 |
[Spring] @Async와 스레드풀 (4) | 2023.08.25 |