13. 쓰레드

KOO HEESEUNG·2021년 10월 13일
0

Java의 정석

목록 보기
7/8
post-thumbnail

프로세스와 쓰레드

프로세스 : 실행 중인 프로그램. 자원(메모리, CPU 등..) + 쓰레드.

쓰레드 : 프로세스의 자원을 이용하여 실제로 작업을 수행하는 것. 모든 프로세스에는 최소 하나 이상의 쓰레드가 존재한다.

프로세스 : 쓰레드 = 공장 : 일꾼

하나의 새로운 프로세스를 생성하는 것보다, 하나의 새로운 쓰레드를 생성하는 것이 더 적은 비용이 든다.


쓰레드의 구현과 실행

구현

  1. Thread 클래스 상속
  2. Runnable 인터페이스 구현

둘 다 run() 메서드만 구현 해주면 되지만, 자바는 다중상속을 허용하지 않기 때문에 2번을 권장한다.

class Ex1 extends Thread { // Thread 클래스 상속
	@Override
	public void run() {...}
}
class Ex2 implements Runnable { // Runnable 인터페이스 구현
	@Override
	public void run() {...}
}

실행

run() 은 클래스에 선언된 메서드를 호출할 뿐, 생성된 쓰레드를 실행시키지 않는다. 쓰레드를 생성한 후 start() 를 호출해야만 쓰레드가 실행된다.

쓰레드는 먼저 start() 를 했다고 해서 반드시 먼저 실행되지는 않는다. 쓰레드의 실행 순서는 OS 스케쥴러가 결정하기 때문이다.

  1. main()start() 호출
  2. start() 가 새로운 호출 스택을 생성.
  3. 새로운 호출스택에 run() 이 호출되고, start() 는 종료됨.
  4. 두 개의 호출스택이 OS 스케쥴러가 정한 순서에 따라 번갈아 실행됨.

img

main 쓰레드

위 그림에서 보다시피 main() 이 있는 호출 스택도 하나의 쓰레드이다. 이를 main 쓰레드 라 한다.

실행중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.

쓰레드는 자신의 작업을 모두 마치면 종료되는데, 다른 사용자 쓰레드가 남아있다면, 프로그램은 종료되지 않는다.


싱글 쓰레드와 멀티 쓰레드

싱글 쓰레드 : 쓰레드가 1개. 순차적으로 작업을 진행하기 때문에, 앞선 작업이 끝나야 다음 작업을 수행한다.

멀티 쓰레드 : 프로세스에 최소 2개 이상의 쓰레드가 존재. 작업을 번갈아서 하여 여러 개의 작업이 동시에 처리되는 것처럼 보인다.

멀티 쓰레드는 싱글 쓰레드보다 작업시간이 조금 더 걸린다. 이는 작업이 비순차적으로 번갈아서 진행될 때, 한 작업에서 다른 작업으로 넘어가는 context switching(문맥 전환) 때문이다. 그러나 멀티 쓰레드는 어떤 작업을 하고 있을 때 동시에 다른 작업을 할 수 있기 때문에 싱글 쓰레드보다 효율적이다.

멀티쓰레드의 장단점

장점

  • CPU의 사용률 향상
  • 자원을 효율적으로 사용
  • 사용자에 대한 응답성 향상
  • 작업이 분리되어 코드가 간결해짐

단점

  • 동기화(synchronization)에 주의
  • 교착상태(dead-lock)에 빠지지 않도록 주의
  • 기아가 발생하지 않도록 작업의 효율적 분배가 필요

I/O 블락킹

I/O 블락킹 : 입출력시 작업 중단.

싱글 쓰레드는 사용자의 입력을 기다리는 동안 다른 작업이 진행되지 않는다.

반면, 멀티 쓰레드는 사용자의 입력을 기다리는 동안 다른 작업을 처리할 수 있다.

쓰레드의 우선순위

작업의 중요도에 따라 쓰레드의 우선순위를 설정할 수 있다. 우선순위의 범위는 1~10이며, 기본 우선순위는 5이다.

void setPriority(int newPriority); // 쓰레드 우선순위를 지정한 값으로 변경
int getPriority(); // 쓰레드의 우선순위 반환

그러나 쓰레드의 우선순위는 OS 스케쥴러가 참고만 하는 것일뿐, 반드시 우선순위가 높은 쓰레드가 먼저 작업을 다 마친다는 것을 보장하지는 않는다.

쓰레드 그룹

쓰레드 그룹 : 서로 관련된 쓰레드의 묶음

Thread(ThreadGroup group, String name);
Thread(ThreadGroup group, Runnable target);
Thread(ThreadGroup group, Runnable target, String name);
Thread(ThreadGroup group, Runnable target, String name, long stackSize);

모든 쓰레드는 반드시 쓰레드 그룹에 포함되어야 한다. 그러나 특정 쓰레드 그룹을 지정해주지 않았다면, 해당 쓰레드를 생성한 쓰레드가 포함되어 있는 쓰레드 그룹으로 자동 지정된다.

데몬 쓰레드

쓰레드에는 사용자 쓰레드(일반 쓰레드)와 데몬 쓰레드(보조 쓰레드) 두 종류가 있다.

데몬 쓰레드는 일반 쓰레드의 작업을 보조해주는 쓰레드로, 자신이 보조하는 일반 쓰레드가 종료되면 데몬 쓰레드도 강제적으로 종료된다. (ex> 가비지 컬렉터, 자동저장, 화면 자동갱신 등)

데몬 쓰레드는 무한루프와 조건문을 이용하여 작성한다. 일반 쓰레드가 작업을 언제 마칠지 모르기 때문에 무한루프를 통해 계속 실행대기하다가 특정 조건을 만족하면 작업을 수행하고, 또다시 대기하도록 한다.

쓰레드는 처음 생성하면 일반 쓰레드로 생성되기 때문에, 데몬 쓰레드로 설정하고 싶다면 반드시 start() 하기 전에 setDaemon() 메서드를 통해 데몬 쓰레드로 변경해주어야 한다.

boolean isDaemon() // 쓰레드가 데몬 쓰레드인지 확인. 반환값 true면 데몬 쓰레드.
void setDaemon(boolean on) // 쓰레드를 데몬 쓰레드 또는 일반 쓰레드로 변경.
  			  // 매개변수 on 값이 true면 데몬 쓰레드.

쓰레드의 상태

img

  1. NEW
    • 쓰레드가 새로 생성되고, 아직 start() 되지 않은 상태
  2. RUNNABLE
    • 쓰레드가 start() 되어 실행중이거나 실행 가능한 상태
    • 실행 가능한 쓰레드는 큐(queue) 구조의 실행대기열에 저장되어 차례가 올 때까지 대기
  3. BLOCKED
    • 동기화 블럭에 의해 일시정지된 상태
    • I/O block
  4. WAITING, TIMED_WAITING
    • 쓰레드 작업이 종료되지는 않았지만, 일시정지된 상태
    • suspend(), sleep(), wait(), join() 등
  5. TERMINATED
    • 쓰레드 작업이 종료된 상태

sleep()

sleep()은 이 메서드를 수행하는 쓰레드를 멈추게(잠들게) 한다.

static void sleep(long millis); // 밀리(1/1000)초 단위
static void sleep(long millis, int nanos);

static 메서드이기 때문에 항상 현재 쓰레드에 대해 동작하므로, 특정 쓰레드를 지정해서 잠들게 하는 것이 불가능하다.

sleep()으로 잠든 상태를 벗어나는 방법은 두 가지이다. 지정한 시간이 종료되었거나, 깨워지는 것이다.

sleep() 을 실행했을 때, InterruptedException 이 발생한다. InterruptedException은 Exception의 자손으로, 잠든 상태를 깨우는 필수예외이다. 때문에 반드시 try-catch 문으로 예외처리를 해주어야 한다. try-catch 문을 이용하여 아래와 같이 메서드를 만들어 사용하는 것이 좋다.

static void delay(long millis) {
	try {
		Thread.sleep(millis)
	} catch (InterruptedException e) {}
}

InterruptedException 은 문제가 있기 때문이 아니라 쓰레드의 잠든 상태를 벗어나게 하는 예외이기 때문에, catch 문 안에 굳이 무언가를 구현해줄 필요는 없다.


interrupt()

대기 상태(WAITING)의 쓰레드를 실행대기 상태(RUNNABLE)로 변경하는 메서드. 쓰레드의 작업을 중단하거나, 중단된 쓰레드를 다시 작업하게 하고 싶을 때 사용한다.

void interrupt();				 // 쓰레드의 interrupted 상태를 true로 변경
boolean isInterrupted(); // 쓰레드의 interrupted 상태를 반환
static boolean interrupted(); // 현재 쓰레드의 interrupted 상태를 반환하고, 그 값을 false로 초기화

isInterrupted() 와 interrupted() 는 둘 다 쓰레드의 상태변수 interrupted 의 값을 반환하지만, 후자는 값 반환 후 그 값을 다시 false 로 초기화한다는 차이가 있다.


suspend(), resume(), stop()

쓰레드의 작업을 각각 일시중지, 재개, 완전 정지 시키는 메서드.

이 메서드들은 교착상태(dead-lock)를 발생시키기 쉽기 때문에 Deprecated(사용 권장하지 않음) 되었다.

사용하기 위해서는 아래와 같이 직접 구현하여야 한다.

class ThreadEx117_1 implements Runnable {
    boolean suspended = false;
    boolean stopped = false;
    
    public void run() {
    	while (!stopped) {
        	if (!suspended) { /* 쓰레드가 수행할 코드 */}    
        }
    }
    public void suspend() { suspended = true; }
    public void resume() { suspended = false; }
    public void stop() { stopped = true; }
}

교착상태(dead-lock)

두 쓰레드가 각자 서로에게 필요한 것을 갖고 있어 작업이 진행되지 않는 상태


join(), yield()

join()은 지정한 시간동안 다른 쓰레드가 작업을 하도록 자신의 작업을 중단하는 메서드이다.

void join(); // 시간을 지정해주지 않으면, 다른 쓰레드가 작업을 마칠 때까지 대기.
void join(long millis);
void join(long millis, int nanos);

join() 과 sleep() 은 거의 비슷하지만, join() 은 다른 쓰레드를 대상으로 하기 때문에 static 메서드가 아니라는 차이가 있다.


yield() 는 현재 쓰레드의 작업시간을 다음 차례의 쓰레드에게 양보하는 메서드이다.

yield() 와 interrupt() 를 적절히 사용하면 프로그램의 응답성을 높이고 효율적인 실행이 가능하다.


쓰레드의 동기화

동기화(synchronization)

멀티 쓰레드 프로세스는 여러 쓰레드가 같은 자원을 공유하기 때문에, 서로의 작업에 영향을 줄 수 있다. 쓰레드의 동기화는 이를 방지하기 위해 다른 쓰레드가 작업에 간섭지 못하게 막는 것을 말한다.

동기화를 하기 위해서는 임계영역이 필요하다. 임계영역이란 간섭받지 않아야 하는 영역을 말하며, 락(lock, 잠금)을 얻은 하나의 쓰레드만 접근이 가능하다.

임계 영역에는 한번에 한 쓰레드만 접근이 가능하기 때문에, "동시에 여러 작업을 수행할 수 있다"는 멀티 쓰레드 프로세스의 장점을 훼손하지 않기 위해 그 영역을 최소화하여야 한다.

임계영역을 설정하기 위해서는 synchronized 를 붙이면 된다. 임계영역을 설정하는 방법은 다음과 같다.

  1. 메서드 전체를 임계 영역으로 지정

    public synchronized void calcSum() { /* 임계 영역 */ }
  2. 특정 영역을 임계 영역으로 지정

    synchronized(객체의 참조변수) { /* 임계 영역 */ }

wait() 과 notify()

동기화는 한번에 한 쓰레드만 접근하도록 하기 때문에 작업의 비효율성을 초래한다. 이러한 단점을 개선하기 위해 wait() 과 notify() 가 등장하였다.

wait() 은 메서드명 그대로 쓰레드를 대기시키는 메서드이다. 임계 영역에 접근하여 작업을 수행중이던 쓰레드가 작업이 불가능한 상황을 맞닥뜨리면, wait() 을 통해 락을 풀고 대기실(waiting pool)로 보내 다른 쓰레드가 작업할 수 있도록 한다. 대기실로 간 쓰레드는 통지를 기다린다.

notify() 는 대기실의 쓰레드에게 통지하는 메서드이다. 임계 영역의 작업을 다시 수행할 수 있는 상태가 되면, notify() 를 통해 대기실의 쓰레드가 다시 락을 얻어 작업을 진행할 수 있다.

notify() 는 대기실 내 임의의 쓰레드에게 통지하는 메서드이고, notifyAll() 은 대기실의 모든 쓰레드에게 통지하는 것이다. 물론 모든 쓰레드가 통지를 받았다 하더라도 임계영역에 접근할 수 있는 쓰레드는 락을 얻은 단 하나뿐이다.

0개의 댓글