스레드
스레드를 이해하기 위해서는 프로세스를 알아야 합니다. 자바 쉽게 배우기 1 - 자바란 무엇인가? 에서 멀티 스레드와, 프로세스, 스케쥴링에 대해 설명드렸었는데 기억하시는 분이 있으실까요?
- 자바의 멀티스레드는 시스템과 관계없이 구현이 가능합니다.
- 멀티 스레드 라이브러리를 제공합니다.
- 여러 스레드에 대한 스케쥴링을 자바 인터프리터가 담당합니다.
❓ 멀티스레드란 무엇인가요?
- 멀티스레드는 한 프로세스 안에서 여러 개의 일이 동시에 진행되는 것을 의미합니다.
❓ 스케쥴링은 무엇인가요?
- 스케쥴링이란 프로세스가 생성되어 실행될 때 필요한 자원들을 할당하는 작업을 뜻합니다.
❓ 프로세스란 무엇인가요?
- 일반적으로 실행 중인 프로그램을 의미하고, 작업(Job) 혹은 태스크(Task)라고 불리기도 합니다.
스레드는 실이라는 뜻을 가지고 있습니다. 여러 코드들이 실처럼 이어져 있기 때문에 붙여진 이름입니다. 스레드를 추가하지 않은 싱글스레드 코드는 한가닥의 실이 쭉 이어진 형태입니다. 하지만 새로운 스레드를 생성하고 실행한다면 그 한가닥의 실에서 가지처럼 뻗어 나온 갈래를 가지게 되겠죠.
처음부터 존재했던 주축이 되는 실을 [메인스레드]라고 부릅니다. 추가한 스레드는 일반적으로 [작업스레드]라 합니다. 각 스레드들은 독립적으로 동시에 처리되면서도, 자원을 공유합니다.
만약, 한 스레드에서 예외가 발생한다면 프로세스 전체가 중단될 수도 있습니다. 그렇기에 신경 써서 예외처리를 해주어야 합니다.
❓ 멀티스레드를 사용하는 이유는 무엇인가요?
멀티스레드를 사용하면 일반적으로 자원(CPU, 메모리 등)을 보다 효율적으로 사용할 수 있습니다.
또한 여러 작업을 동시에 할 수 있기 때문에 사용자 요청에 대한 반응이 빨라집니다.
하지만 항상 멀티스레드가 싱글스레드보다 좋은 성능 보이는 것은 아닙니다. 멀티스레드를 사용할 때 작업 전환 (context switching)에 시간이 소요되고, 스레드 간 자원 경쟁 때문에 더 낮은 성능을 보일 수도 있습니다.
스레드 구현
1. Runnable 인터페이스 구현
Thread thread = new Thread(Runnable target);
Runnable 인터페이스를 구현한 target 객체를 매개변수로 하여 스레드를 만듭니다.
Thread thread = new Thread(new Runnable() {
public void run() {
...
}
});
target 대신 익명객체를 사용할 수 있습니다. 훨씬 간편하죠?
2. Thread 클래스 상속
public class WorkerThread extends Thread {
@Override
public void run() {
...
}
}
Thread thread = new WorkerThread();
Thread를 상속받는 클래스를 통해 스레드를 만듭니다.
Thread thread = new Thread() {
public void run() {
...
}
}
마찬가지로 익명객체를 사용할 수 있습니다.
스레드 사용
다양한 방법으로 구현한 스레드를 사용하려면 start() 메서드를 호출합니다.
Thread thread = new Thread(...);
thread.start();
❓ 왜 run()을 호출하지 않고 start()를 호출하나요?
새로운 스레드가 작업을 실행하기 위해서는 새로운 호출스택이 필요합니다. start()는 새로운 호출스택을 만들고 그 호출스택에 run()이 올라가도록 합니다.
스레드 이름
메인 스레드의 이름은 main, 새로 추가한 스레드의 이름은 Thread-n으로 자동 생성됩니다.
setName()으로 이름을 지정하고, getName()으로 이름을 불러올 수 있습니다.
스레드 우선순위
스레드마다 우선순위를 다르게 하고 싶다면, 즉 더 빨리 처리되어야 할 작업 스레드가 있을 때 우선순위를 지정할 수 있습니다.
setPriority(int newPriority)으로 우선순위를 지정하고, getPriority()으로 우선순위를 불러올 수 있습니다.
우선순위는 1부터 10까지 지정 가능하며, 10이 가장 높은 우선순위를 가집니다. main 스레드의 우선순위는 5입니다.
스레드 실행제어
스레드의 상태
생성 (NEW) > 실행대기 (RUNNABLE) > 실행 > 소멸(TERMINATED)
스레드 객체를 생성하면 생성(NEW) 상태, start() 메서드를 호출하면 실행대기(RUNNABLE) 상태가 됩니다. 실행대기열은 큐 구조로 되어 있습니다. 운영 체제가 실행 대기 상태에 있는 스레드 중 하나를 선택해 실행 상태로 만듭니다.
❗ 실행 중 일시정지(WATING, TIMED_WAITING, BLOCKED) 될 수 있습니다. 일시정지가 끝나면 다시 실행대기열에 추가됩니다.
❗ 일시정지 : sleep(), join(), suspend(), wait(), I/O block
❗ 실행대기 : start(), time-out, interrupt(), resume(), yield(), notify(),
실행제어와 관련된 메서드
메서드 | 설명 |
static void sleep(long millis) static void sleep(long millis, int nanos) |
지정된 시간 (밀리초) 동안 스레드를 일시정지 시킵니다. 코드가 있는 스레드에 대해 작동 |
void join() void join(long millis) void join(long millis, int nanos) |
스레드의 작업을 일시정지하고 지정된 시간동안 다른 스레드를 실행 시킵니다. |
void interrupt() | 일시정지 상태가 아니라면 interrupted 상태를 true로 변경시킵니다. 일시정지 상태에 있을 때 InterruptedException을 발생시키고, 실행대기 상태로 변경합니다. interrupted 를 false로 초기화합니다. interrupted() : interrupted 값을 반환하고 false로 초기화합니다. isInterrupted() : interrupted 값을 반환합니다. |
static void yield() | 실행 중에 자신에게 주어진 실행시간을 다른 스레드에게 양보하고 자신은 실행대기 상태가 됩니다. |
예외 처리 해야 하는 메서드
권장하지 않는 메서드
동기화
멀티스레드 환경에서 공유하는 자원이 있을 때 데이터 무결성을 위해 동기화가 필요합니다. 스레드의 동기화란 공유 데이터를 사용할 때 스레드의 작업이 끝날 때까지 잠금을 걸어 다른 스레드가 사용할 수 없도록 하는 것입니다.
이때, 공유 데이터를 사용하는 코드 영역을 임계영역이라고 합니다.
동기화 메서드
public synchronized void method() {
// 임계 영역
}
특정 영역 지정
synchronized(객체의 참조변수) {
// 임계 영역
}
동기화 메서드가 여러 개 있을 경우 한 스레드가 이들 중 하나를 실행할 때 다른 스레드는 모든 동기화 메서드를 실행할 수 없습니다.
wait(), notify()
한 스레드가 너무 오래 락을 차지하지 않도록 하기 위한 메서드들입니다. 스레드가 임계 영역의 코드를 실행하다가 더 이상 작업할 수 없는 상태가 됐을 때 wait() 메서드를 호출하여 다른 스레드에게 잠금을 풀어줍니다. 다시 작업할 수 있는 상태가 되었을 때 notify() 메서드로 락을 가져옵니다.
다만, 이 방법에는 한계가 있습니다. wait() 메서드를 호출한 스레드들이 여럿일 때 notify()는 락이 우선적으로 필요한 스레드에게 락을 주는 것이 아니라 임의의 스레드에게 부여합니다. 굉장히 비효율적이겠죠?
Lock, Condition
위와 같은 문제를 해결하기 위한 클래스들입니다.
클래스 | 설명 |
ReentrantLock | 재진입이 가능한 lock입니다. |
ReentrantReadWriteLock | 읽기에는 공유적이고, 쓰기에는 배타적인 lock입니다. |
StampedLock | ReentrantReadWriteLock에 낙관적인 lock 기능을 추가했습니다. |
스레드와 관련된 많은 내용을 한 번에 정리해 보았습니다. 프로세스는 스레드라는 흐름에 의해 실행되고, 한 프로세스가 여러 스레드를 가질 수 있습니다. 효율적인 자원 사용이나 빠른 응답이 필요할 때 스레드를 사용합니다.
스레드를 사용하기 위해서는 흐름이 원활하게 흐르도록 예외 처리에 신경 써야 합니다. CPU의 코어수, OS의 스케쥴링 방법 등 스레드 결과를 예측하기 어렵게 만드는 요인이 많습니다.
스레드를 이해하고 사용하는 건 어려울 수밖에 없습니다. 완벽하게 이해하려고 하기보다는 감을 잡는 것에 초점을 맞추는 것이 좋을 것 같습니다. 다음 글에서는 람다에 대해 설명합니다.
'개발언어 > Java : 자바' 카테고리의 다른 글
자바 쉽게 배우기 22 - 입출력 스트림 (0) | 2023.09.19 |
---|---|
자바 쉽게 배우기 21 - 람다 (0) | 2023.09.18 |
자바 쉽게 배우기 19 - 열거형 (Enum) (0) | 2023.02.21 |
자바 쉽게 배우기 18 - 지네릭스 (0) | 2023.02.20 |
자바 쉽게 배우기 17 - Arrays (0) | 2023.02.14 |