본문 바로가기
개발언어/Java : 자바

자바 쉽게 배우기 20 - 스레드

by 개발자D 2023. 9. 15.

스레드

스레드

스레드를 이해하기 위해서는 프로세스를 알아야 합니다. 자바 쉽게 배우기 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 값을 반환합니다.
void stop() 스레드를 종료시킵니다.
void suspend() 스레드를 일시정지 시킵니다.
void resume() suspend()에 의해 일시정지된 스레드를 실행 대기 상태로 만듭니다.
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의 스케쥴링 방법 등 스레드 결과를 예측하기 어렵게 만드는 요인이 많습니다.

스레드를 이해하고 사용하는 건 어려울 수밖에 없습니다. 완벽하게 이해하려고 하기보다는 감을 잡는 것에 초점을 맞추는 것이 좋을 것 같습니다. 다음 글에서는 람다에 대해 설명합니다.

 

자바 쉽게 배우기 21 - 람다

잠시 쉬어가는 글로 람다에 대해서 설명드릴까 합니다. 사실 람다가 자바에 도입된 것은 jdk1.8부터(2014년)입니다. 비교적 최근에 추가된 것이죠. 람다가 도입되면서 객체 지향 언어인 자바가 함

devdharu.tistory.com