본문 바로가기

Java

Java 쓰레드

쓰레드

 

 

1.쓰레드

 

프로세스(Process)는 실행중인 프로그램을 의미한다.

프로그램을 실행하게 되면, OS로부터 필요한 자원(메모리)를 할당받아 프로세스가 된다.

 

프로세스는 프로그램을 수행하기 위해서

필요한 데이터와 메모리 등의 자원, 그리고 쓰레드로 구성되어 있다.

 

쓰레드는 프로세스의 자원을 이용해 작업을 실제로 수행한다.

 

예시 ) 프로그램을 실행하기 위해서 프로세스라는 공장에서

작업을 처리하는 일꾼이 쓰레드이다. 멀티쓰레드는 일꾼이 늘어난 것이다.



멀티 쓰레딩의 장단점

① CPU의 사용률을 향상시킨다

② 자원을 보다 효율적으로 사용할 수 있다

③ 사용자에 대한 응답성이 향상된다

④ 작업이 분리되어 코드가 간결해진다

 

메신저로 채팅을 하면서 동시에 유튜브를 볼 수 있는 것은 

멀티쓰레드로 작성되어 있기 때문이다.

 

그러나, 멀티쓰레딩도 단점이 있다.

프로세스 내 여러 쓰레드가 자원을 공유하면서 작업하기 때문에

동기화, 교착상태와 같은 문제가 발생할 수도 있다.



1) 쓰레드의 구현과 실행

 

① 쓰레드를 구현하는 방법 - Thread 클래스 상속 or Runnable 인터페이스를 구현

 

어느 것을 선택해도 괜찮지만,

Thread 클래스 상속받는 경우 다른 클래스를 상속받을 수 없다.

이로 인해 Runnable 인터페이스를 구현하는 게 일반적이다.

쓰레드를 구현하는 방법  Thread 클래스 상속 or Runnable 인터페이스를 구현

 

Runnable 인터페이스는 오로지 run()만 정의된 인터페이스다.

Runnable 인터페이스를 구현하기 위해서 해야 할 일은

추상 메서드인 run()의 몸통{}, body를 만들어주는 것이다.

 

Thread 클래스 상속 or Runnable 인터페이스를 구현이든, 

쓰레드를 통해 작업하고자 하는 내용으로 run()의 몸통을 채우면 된다.



Thread 클래스 상속과  Runnable 인터페이스 구현 시 인스턴스 생성 방법이 다르다.

 

-->Thread 클래스 상속

ThreadEx1_1 t1 = new ThreadEx1_1();

 

-->Runnable 인터페이스 구현

Runnable r = new ThreadEx1_2();  // Runnable을 구현한 클래스의 인스턴스 생성 

Thread t2 = new  Thread(r);  // Thread 생성자에 Runnable 참조변수 대입

 

Thread t2 = new Thread (new ThreadEx1_2()); //위의 두줄을 한줄로 축약

 

쓰레드를 구현하는 방법  Thread 클래스 상속 or Runnable 인터페이스를 구현



② 쓰레드의 실행하는 방법 - start()

 

쓰레드를 생성했다고 자동적으로 실행되지 않는다.

start()를 호출해야만 쓰레드가 실행된다.

 

ThreadEx1_1 t1 = new ThreadEx1_1();  //쓰레드 t1을 생성한다.

t1.start(); //쓰레드 t1을 호출한다.

 

사실, start()가 호출되었다고 바로 실행되지 않는다.

일단 실행대기 상태에 있다가 자신의 차례가 돼야 실행된다. 

물론 실행중인 쓰레드가 하나도 없으면 곧바로 실행된다. 

 

주의할 점은 한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다는 것이다.

즉 하나의 쓰레드에 대해 start()가 한 번만 호출될 수 있다는 것을 의미한다.

 

만약 쓰레드의 작업을 한 번 더 수행해야 한다면 어떻게 해야 할까?

한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다

위와 같이 새로운 쓰레드를  생성한 다음에 start()를 호출해야 한다.

그렇지 않으면 IllegalThreadStateException이 발생한다.



start() vs run()

 

쓰레드를 실행시킬 때 run()이 아닌 start()를 호출시키는 이유가 무엇일까?

 

만약 main 메서드에서 run()을 호출하게 되면

생성된 쓰레드를 호출하는 게 아니라 단순히 클래스에 선언된 메서드를 호출하는 것이다.

 

반면에 start()는 새로운 쓰레드가 작업을 실행하는데

필요한 호출스택을 생성한 다음에, run()을 호출하여 생성된 호출스택에

run()이 첫 번째로 올라가게 한다.

모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 요구하기 때문에,

새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출스택이 생성되고

쓰레드가 종료되면 해당 호출 스택은 사라진다.



cf. main쓰레드

 

main메서드의 작업을 수행하는 것도 쓰레드이며, 이를 main쓰레드라고 한다.

쓰레드는 프로세스라는 공장에서 일하는 일꾼인데, 최소한 한명의 일꾼은 필요하다.

그래서 프로그램을 실행하면 하나의 쓰레드(일꾼)을 생성하고,

그 쓰레드가 main메서드를 호출해서 작업이 수행되도록 하는 것이다.

 

실행 중인 사용자 쓰레드(non-daemon thread)가 하나도 없을 때 프로그램을 종료된다.

 

2) 싱글쓰레드와 멀티쓰레드

 

두 개의 작업을 하나의 쓰레드로 처리하는 경우와

두 개의 작업을 두 개의 쓰레드로 처리하는 경우를 가정해보자.

 

하나의 쓰레드로 두 작업을 처리하는 경우 한 작업을 마친 후에 다른 작업을 시작하지만,

두 개의 쓰레드로 작업하는 경우에는 짧은 시간동안 2개의 쓰레드가 번갈아가며

작업을 수행하여 동시에 두 작업이 처리되는 것처럼 느껴진다.

 

두 개의 쓰레드로 작업한 시간이 싱글 쓰레드로 작업한 시간보다 더 걸린다.

그 이유는 쓰레드간의 작업 전환에 시간이 걸리기 때문이다. 

 

하나의 쓰레드로 두 작업을 처리하는 경우 한 작업을 마친 후에 다른 작업을 시작
두 개의 쓰레드로 작업하는 경우에는 짧은 시간동안 2개의 쓰레드가 번갈아가며 작업을 수행

 

 

 

3) 쓰레드의 I/O 블락킹(blocking)

 

두 쓰레드가 서로 다른 자원을 사용하는 작업의 경우

멀티쓰레드 프로세스가 더욱 효율적이다. 

 

예를 들어, 데이터로부터 입력받는 작업, 네트워크로부터 파일을 주고 받는 작업,

프린터로 파일을 출력하는 작업처럼 외부기기와의 입출력을 필요로하는 경우가 해당된다.

 

싱글쓰레드 프로세스는 사용자로부터 입력을 받는 작업과 화면에 출력하는 작업을

하나의 쓰레드로 처리하면 입력 받는 동안 아무 일도 하지 못하고 대기해야 한다.

 

반면 멀티쓰레드 프로세스는 사용자의 입력을 기다리는 동안

다른 쓰레드가 작업을 수행할 수 있기 때문에 보다 효율적이다.

 

입출력 처리를 위해 쓰레드가 기다리는 것을 I/O 블락킹이라고 한다.

두 쓰레드가 서로 다른 자원을 사용하는 작업의 경우 멀티쓰레드 프로세스가 더욱 효율적이다.
쓰레드의 I/O 블락킹(blocking)
쓰레드의 I/O 블락킹(blocking)

 

 

13_5.

이전 예제와 달리 사용자로부터 입력받는 부분과

화면에 숫자를 출력하는 부분이 두개의 쓰레드로 나누어 처리되었다.

이로 인해 사용자가 입력을 끝내지 않았지만 화면에 숫자가 출력된다.

 

4) 쓰레드의 우선 순위

 

쓰레드는 우선순위(priority)라는 속성(멤버변수)를 가지고 있다.

즉, 쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 지정하여

특정 쓰레드가 더 많은 작업 시간을 갖도록 설정할 수 있다.

 

예를 들어, 파일 전송 기능이 있는 메신저의 경우

파일 다운로드를 처리하는 쓰레드보다 채팅을 전송하는 쓰레드의 우선순위가 높아야

사용자의 불편함이 없을 것이다.

 

따라서 시각적인 부분, 혹은 사용자에게 빠르게 반응해야하는 작업을 하는

쓰레드의 우선순위는 다른 작업을 수행하는 쓰레드에 비해 우선순위가 높아야 한다.

 

쓰레드의 우선순위와 관련된 메서드와 상수

쓰레드의 우선순위와 관련된 메서드와 상수

쓰레드가 가질 수 있는 우선순위의 범위는 1~10 (숫자가 높을수록 우선순위 높음)

 

쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받는다.

main 메서드를 수행하는 쓰레드는 우선순위가 5이므로,

main메서드 내에서 생성하는 쓰레드의 우선순위는 5가 된다.

 

 

쓰레드의 우선순위와 관련된 메서드와 상수

 

5 )쓰레드 그룹(thread group)

 

폴더를 생성해 관련된 파일을 함께 다룰 수 있는 것처럼

쓰레드 그룹을 생성해 쓰레드를 그룹으로 묶어 관리할 수 있다.

 

폴더 안에 또 다른 폴더를 생성할 수 있는 것처럼

쓰레드 그룹 안에 또다른 쓰레드 그룹을  포함시킬 수 있다.

 

쓰레드 그룹은 보안을 위해 도입된 개념이기 때문에

자신이 속한 쓰레드 그룹 혹은 하위 쓰레드 그룹은 변경할 수 있지만

다른 쓰레드 그룹의 쓰레드를 변경할 수는 없다.

 

모든 쓰레드는 반드시 쓰레드 그룹에 포함되어야 한다.

만약 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는

자신을 생성한 쓰레드와 같은 쓰레드 그룹에 속하게 된다.

 

 

자바 어플리케이션이 실행되면, JVM은 main과 system이라는 쓰레드 그룹을 만들고

JVM운영에 필요한 쓰레드들은 생성해서 이 쓰레드 그룹에 포함시킨다.

 

예를 들어 main메서드를 수행하는 main이라는 이름의 쓰레드는 main 쓰레드 그룹에 속하고,

가비지컬렉션을 수행하는 Finalizer쓰레드는 system쓰레드 그룹에 속한다.

우리가 생성하는 모든 쓰레드 그룹은 main쓰레드 그룹의 하위 쓰레드 그룹이 되며,

쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 자동적으로 main쓰레드 그룹에 속하게 된다. 

 

-->쓰레드 그룹과 관련된 메서드

 

ThreadGroup getThreadGroup()  쓰레드 자신이 속한 쓰레드 그룹을 반환

void uncaughtException(Thread t, Throwable e) 처리되지 않은 예외에 의해 쓰레드 그룹의 쓰레드가 실행 종료될 때,                                                                    JVM에 의해 이 메서드가 자동적으로 호출된다. 

ThreadGroup(String name) 지정된 이름의 새로운 쓰레드 그룹을 생성

ThreadGroup(ThreadGroup parent, String name) 지정된 쓰레드 그룹에 포함되는 새로운 쓰레드 그룹을 생성

 

 

6) 데몬 쓰레드(daemon thread)

 

쓰레드는 데몬 쓰레드와 일반 쓰레드로 분류된다.

데몬 쓰레드는 일반 쓰레드의 작업을 돕는 보조 역할을 수행한다.

 

따라서 일반쓰레드가 종료되면, 데몬 쓰레드는 강제적으로 종료된다.

데몬 쓰레드는 일반 쓰레드의 보조 역할을 수행하므로

일반 쓰레드의 실행 종료 시 의미를 상실하기 때문이다.

 

데몬 쓰레드 예시 ) 가비지 컬렉터, 워드프로세서의 자동저장, 화면 자동 갱신 등

 

데몬 쓰레드는 무한루프와 조건문을 이용해 실행 후

대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성돼야 한다.

 

데몬 쓰레드는 일반 쓰레드의 작성법과 동일하지만,

쓰레드를 생성 후 다음 실행하기 전에 setDemon(true)를 호출하기만 하면 된다.



cf. 데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 된다는 것에 유의하자.

데몬 쓰레드는 일반 쓰레드의 작업을 돕는 보조 역할을 수행한다.

 

boolean isDaemon() 쓰레드가 데몬쓰레드인지 확인한다. 데몬 쓰레드이면 true반환

 

void setDaemon(boolean on) 쓰레드를 데몬 쓰레드 혹은 사용자 쓰레드로 변경

      매개 변수 on의 값이 true로 지정하면 데몬 쓰레드가 된다.

 

 

데몬 쓰레드는 일반 쓰레드의 작업을 돕는 보조 역할을 수행한다.




7) 쓰레드의 상태

 

상태 설명
NEW 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태
RUNNABLE 실행중 또는 실행 가능한 상태
BLOCKED 동기화블록에 의해 일시정지된 상태(lock이 풀릴 때까지 대기하는 상태)
WAITING,
TIMED-WAITING
쓰레드의 작업이 종료되지 않았지만 실행불가능한 일시정지 상태,
TIMED-WAITING은 일시정지시간이 지정된 경우
TERMINATED 쓰레드의 작업이 종료된 상태

쓰레드의 상태

① 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 게 아니라

실행대기열에 저장돼 자신의 차례가 될 때까지 기다린다.

실행대기열은 큐와 같은 구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행된다.

 

② 실행대기상태에 있다가 자신의 차례가 되면 실행상태가 된다

 

③ 주어진 실행시간이 다되거나 yield()를 만나면 다시 실행대기상태가 되고

다음 차례의 쓰레드가 실행상태가 된다

 

④ 실행 중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있다.

I/O block은 입출력작업에서 발생하는 지연상태를 의미한다.

사용자의 입력을 기다리는 경우인데, 이때 일시정지 상태에 있다가

사용자가 입력을 마치면 다시 실행대기 상태가 된다.

 

⑤ 지정된 일시정지시간이 다 되거나(time-out), notify(), resume(), interrupt()가 호출되면

일시정지 상태를 벗어나 다시 실행대기열에 저장돼 자신의 차례를 기다리게 된다.

 

⑥ 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸된다.



7-2) 쓰레드의 실행제어

쓰레드의 스케줄링을 효율적으로 관리하기 위해서는

쓰레드의 상태와 관련된 메서드에 대한 숙지가 요구된다.



메서드 설명
static void sleep(long millies)
static void sleep(long millies, int nanos)
지정된 시간(천분의 일초 단위)동안 쓰레드를 일시정지시킨다. 지정된 시간이 지나고 나면 자동적으로 다시 실행대기상태가 된다.
void join()
void join(long millies)
void join(long millies, int nanos)
지정된 시간동안 쓰레드가 실행되도록 한다. 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속한다.
void interrupt() sleep()이나 join()에 의해 일시정지상태인 쓰레드를 깨워서 실행대기상태로 만든다. 해당 쓰레드에서는 INterruptedException이 발생함으로써 일시정지상태를 벗어나게 한다.
void stop() 쓰레드를 즉시 종료시킨다.
void suspend() 쓰레드를 일시정지시킨다. resume()을 호출하면 다시 실행대기상태가 된다.
void resume() suspend()에 의해 일시정지상태에 있는 쓰레드를 실행대기상태로 만든다.
static void yield() 실행중에 자신에게 주어진 실행시간을 다른 쓰레드에게 알림으로써 양보(yield)하고 자신은 실행대기상태가 된다.

 

cf. resume(), stop() , suspend()는 쓰레드를 교착상태로 만들 우려가 있어 deprecated되었다.

deprecated된 메서드는 하위 호환성을 위해서 삭제하지 않은 것일 뿐, 사용해서는 안 된다.

 

sleep() - 지정된 시간동안 쓰레드를 멈추게 한다.

밀리세컨드(millies, 1000분의 일초)와 나노세컨드(nanos, 10억분의 일초)의 시간단위로

값을 지정할 수 있으나 오차발생 가능성 있다.

 

sleep() 을 호출할 때는 항상 try -catch문으로 예외를 처리해줘야 한다.

 

쓰레드의 실행제어 메서드 활용 예시



interrupt() - 쓰레드에게 작업을 멈추라고 요청

멈추라고 요청할 뿐, 강제적으로 종료시키진 못한다.

 

그리고 Interrupted() 는 쓰레드에 대해 interrupt()가 호출되었는지 알려준다.

호출되지 않았으면  false, 호출되었으면 true를 반환한다.

 

void interrupt() 쓰레드의 interrupted상태를 false에서 true로 변경

boolean isInterrupted() 쓰레드의 interrupted상태를 반환

static boolean interrupted() 현재 쓰레드의  interrupted상태를 반환 후, false로 변경



한 쓰레드가 sleep(), wait(), join()에 의해 일시정지 상태(WAITING)에 있을 때

이 쓰레드에 대해 interrupt()를 호출하면  sleep(), wait(), join()에 대해

InterruptedException이 발생하고, 이 쓰레드는 실행대기 상태로 바뀐다.

즉 멈춰있던 쓰레드를 깨워서 실행가능한 상태로 만드는 것이다.



쓰레드의 실행제어 메서드 활용 예시

 

join()과 yield()

 

join()  - 다른 쓰레드의 작업을 기다린다.

쓰레드 자신이 하던 작업을 잠시 멈추고,

다른 쓰레드가 지정된 시간동안 작을 수행하도록할 때 join()을 사용한다.

 

예를 들어

 

void join()

void join(long millis)

void join(long millis, int nonos)

 

시간을 지정하지 않는다면 해당 쓰레드가 작업을 모두 마칠 때까지 기다리게 한다.

작업중에 다른 쓰레드의 작업이 먼저 수행돼야 할 때 join()을 사용한다.

 

작업중에 다른 쓰레드의 작업이 먼저 수행돼야 할 때 join()을 사용한다.

 

join()도 sleep()처럼 interrupt()에 의해 대기상태에서 벗어날 수 있으며,

join()이 호출되는 부분을 try - catch문으로 감싸야 한다.

 

단, sleep()이 현재 실행중인 쓰레드에 대해 작동하는 반면

join()은 특정 쓰레드에 대해 동작하므로 static 메서드가 아니다.

 

yield() - 다른 쓰레드에게 양보한다

 

자신에게 주어진 실행시간을 다음 차례 쓰레에게 양보한다.

예를 들어, 스케줄러로부터 3초를 할당받았는데 0.5초에 yield()가 호출되면

나머지 시간을 포기하고 다시 실행대기상태가 된다.

 

작업중에 다른 쓰레드의 작업이 먼저 수행돼야 할 때 join()을 사용한다.



8) 쓰레드의 동기화(Synchronization)

 

멀티쓰레드 프로세스의 경우 여러 쓰레드가 동일한 프로세스 내의 자원을

공유하여 작업하기 때문에 서로의 작업에 영향을 줄 수 있다.

이를 방지하기 위해 한 쓰레드가 특정 작업을 마치기 전까지

다른 쓰레드에 의해 방해받지 않도록 하는 게 필요하다. 

 

이로 인해 도입된 개념이 ‘임계 영역’과 잠금(lock)이다.

 

공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정하고,

공유 데이터가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한다.

 

그리고 해당 쓰레드가 임계 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납해야만

다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 있게 된다. 



‘쓰레드의 동기화’ →한 쓰레드가 진행중인 다른 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것

 

① synchronized 키워드를 이용한 동기화  —> 임계 영역 설정

synchronized 키워드를 이용한 동기화  임계 영역 설정

 

synchronized 키워드를 이용한 동기화 임계 영역 설정

잔고가 음수가 되는 것을 볼 수 있는데,

그 이유는 한 쓰레드가 if문이 조건식을 통과하고 출금하기 바로 직전에

다른 쓰레드가 끼어들어 출금을 먼저 했기 때문이다.

 

synchronized 키워드를 이용한 동기화 임계 영역 설정

 

② wait()과 notify()

 

synchronized 를 통해 공유 데이터를 보호하는 것은 좋지만,

특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간 보내지 않도록 주의해야 한다.

다른 쓰레드들이 해당 객체의 락을 기다리느랴 다른 작업을 원할히 진행하지 못하기 때문이다.

 

이런 상황에서 고안된 게 wait()와 notify()이다.

 

wait() - 동기화된 임계영역 코드를 수행하다가

작업을 더 이상 진행할 수 있는 상황이 아니면,

 

wait()을 호출하여 쓰레드가 락을 반납하고 기다리게 한다.

그러면 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 수행할 수 있다.

 

이후, 작업을 진행할 수 있는 상황이 되면 notify()를 호출해서

작업을 중단했던 쓰레드가 다시 락을 얻어 작업을 진행할 수 있게 한다. 

 

wait()가 호출되면 실행중이던 쓰레드는 해당 객체의 대기실에서 통지를 기다린다.

 

notify()가 호출되면, 해당 객체의 대기실에 있던 모든 쓰레드 중에서

임의이 쓰레드만 통지를 받는다. notifyAll()은 기다리고 있는 모든 쓰레드에게 통보를 한다.

 

 

wait()과 notify()는 특정 객체에 대한 것이므로 Object 클래스에 정의됐다.

wait()은 notify() or notifyAll()이 호출될 때까지 기다리지만,

매개변수가 있는 wait()은 지정된 시간동안만 대기한다.



wait(), notify(), notifyAll()

-->Object에 정의돼 있다.

-->동기화 블록내에서만 사용할 수 있다.

-->보다 효율적인 동기화를 가능하게 한다.

wait()과 notify()

 

wait()과 notify()
wait()과 notify()

 

'Java' 카테고리의 다른 글

Java 람다와 스트림 -(2)  (0) 2022.05.04
Java 람다와 스트림 - (1)  (0) 2022.05.02
Java 지네릭스, 열거형, 애너테이션  (0) 2022.04.28
Java 컬렉션프레임웍 - (2)  (0) 2022.04.27
Java 컬렉션 프레임웍 - (1)  (0) 2022.04.26