본문 바로가기

Advanced java

Java Thread API

오늘은 자바의 Thread API 의 메소드들과 그 사용법에 대해 간단히 정리하고자 한다.


1. 쓰레드의 상태제어 (간단한 메소드는 생략했습니다.)


1) interrupt() : "일시정지 상태의" 쓰레드에서 InterruptException 예외를 발생시켜 예외처리 코드에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 한다.


- 스레드의 종료를 위해서는 몇가지 방법이 있다, 그 중 하나는 stop() 메소드를 호출하는 것 인데, stop() 메소드는 스레드가 처리하던 작업의 뒷처리를 해주지 않고 종료되어 버리므로 불안정하다. 그 기능을 도와주는 메소드가 바로 interrupt() 이다. 스레드를 종료시키기 위해 해당쓰레드.interrupt() 하면 해당 쓰레드가 "일시정지 상태에 있을 때" InterruptedException을 던지고 종료된다.


여기서 문제점은 "일시정지 상태가 없다면" 예외를 던지지 않는다는 점이다. 이를 위한 두가지 방법이 있는데 Thread 클래스에서 제공하는 interrupted() 메소드나 각 Thread 객체에 존재하는 isInterrupted() 메소드를 사용하는 것 이다. 이 둘중 어떤 메소드를 사용해도 관계없다. 단지 실행문을 돌면서 Interrupt가 호출 되었는지 아닌지 확인하고 자원을 정리하고 쓰레드를 종료하면 되는 것 이다.


2) notify(), notifyAll() : "동기화 블록 내에서" wait() 메소드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만든다.


3) wait(), wait(long millis), wait(long millis, int nanos) : "동기화 블록 내에서" 스레드를 일시정지 시킨다. 매개값으로 주어진 시간이 지나면 자동적으로 실행 대기상태가 된다. 시간이 주어지지 않으면 notify, notifyAll 메소드에 의해 실행 대기상태로 갈 수 있다.


- 경우에 따라서 두개의 쓰레드를 교대로 번갈하가며 실행해야할 경우가 있다. 정확한 교대작업이 필요 할 경우, 자신의 작업이 끝나면 상대방 스레드를 일시정지 상태에서 풀어주고, 자신은 일시 정지 상태로 만드는 것 이다.


이 방법의 핵심은 공유객체에 있다. 공유 객체에는 두 스레드가 작업 할 내용을 각각 동기화 메소드로 구분해 놓는다. 각 메소드에서는 해당 메소드를 실행한 객체가 작업을 마치면 notify()를 호출하여 다른 스레드 중 하나를 실행 대기 상태로 만들고, 자신은 두번 작업하지 않도록 wait()를 호출한다.


4) join(),join(Long millis),join(Long millis, int nanos) : join 메소드를 호출한 스레드는 일시정지 상태가 된다. 실행 대기상태로 가려면, join() 메소드를 멤버로 가지는 스레드가 종료되거나 매개값으로 주어진 시간이 지나야 한다.

- 스레드는 다른 스레드와 독립적으로 실행되야 하는게 기본이지만 다른 스레드가 종료 될 때마다 기다렸다가 실행해야 하는 경우가 발생 할 수 있습니다. 이럴 경우를 대비하여 Thread 클래스에서는 join() 을 제공하고 있습니다.


ThreadA ThreadB 가 존재할 때, ThreadA 객체가 실행되고 ThreadB객체.join()을 한다면 ThreadB 객체가 종료 되기 전까지 ThreadA는 계속 기다리게 됩니다. 그리고 ThreadB가 종료되면 ThreadA는 다음 실행문을 진행하게 됩니다.


5) yield() : 실행중에 우선순위가 동일한 다른 스레드에세 실행을 양보하고 실행 대기상태가 된다.


- 스레드가 처리하는 작업은 반복적인 실행을 위해 for문이나, while문을 포함하는 경우가 많다. 가끔은 무의미한 반복을 하는 경우도 있다. 예제코드를 살펴보자.




이런 코드에서 work가 false이고, false에서 true로 가는 시점이 불분명하다면 while문은 어떠한 실행문도 실행시키지 않고 무의미한 반복만 계속 하게 될 것이다. 
그러면 멀티 쓰레드 환경에서 무의미하게 실행시간을 잡아먹는 경우가 생긴다. 
이런 상황을 피하기 위해서 yield() 메소드를 사용한다. 
그렇게 되면 work가 false일때는 while문이 더이상 돌지 않고 다른 쓰레드에게 실행을 양보하게 된다.

예제코드



2. 쓰레드 그룹(Thread Group)


쓰레드 그룹은 관련된 쓰레드를 묶에서 관리할 목적으로 이용된다. JVM이 실행되면 system 스레드 그룹을 만들고 JVM 운영에 필요한 스레드들을 생성해서 system 스레드 그룹에 포함시킨다. 그리고 system 하위 스레드 그룹으로 main을 만들고, 메인 스레드를 main스레드 그룹에 포함시킨다. 스레드는 반드시 하나의 그룹에 포함되는데, 명시적으로 스레드 그룹에 포함시키지 않으면 기본적으로 자신을 생성한 스레드 그룹에 속하게 된다.


현재 스레드가 속한 스레드 그룹의 이름은 얻고 싶다면 다음과 같이 할 수 있다.


- 쓰레드 그룹 생성


명시적으로 쓰레드 그룹을 만들고 싶다면 다음의 생성자를 사용해서 쓰레드 그룹을 만들 수 있다. 쓰레드 그룹 생성시 부모 스레드 그룹을 지정하지 않으면 현재 쓰레드가 속한 그룹의 하위 그룹이 된다.



스레드를 스레드 그룹에 포하시키면 어떤 이점 있을까? 스레드 그룹에서 제공하는 interrupt() 메소드를 이용하면 그룹내에 포함된 모든 스레드들을 한번에 interrupt 할 수 있다. 하지만 그룹에서 제공하는 interrupt() 메소드는 단지 호출만 할 뿐이므로 이후 처리는 각각의 스레드에서 해 주어야 한다.


3. 스레드풀


병렬처리가 많아지면 스레드 개수가 증가하고, 그에 따른 스레드 생성과 스케줄링으로 인해 애플이케이션의 성능이 저하된다. 갑작스러운 병렬작업의 폭증으로 인한 스레드의 폭증을 막으려면 스레드풀을 사용해야 한다.


스레드풀은 작업처리에 사용되는 스레드를 제한된 개수만큼 정해놓고, 작업큐에 들어오는 작업들을 하나씩 스레드가 맡아 처리한다. 작업처리가 끝난 스레드는 다시 작업큐에서 새로운 작업을 가져와 처리한다. 즉 스레드풀에는 정해놓은 갯수만큼에 스레드가 들어있고, 작업 요청이 들어오면 작업큐에 있는 작업들을 쓰레드 갯수만큼 pop해서 처리하게 된다. 따라서 병렬처리가 폭증하더라고 스레드의 갯수는 증가하지 않게 된다.


- 스레드풀 생성


자바는 스레드풀을 생성하고 사용 할 수 있도록 java.util.concurrent 패키지에서 ExcutorService 인터페이스와 Excutors 클래스를 제공하고 있다. ExcutorService 의 구현객체는 다음 두가지 정적 메소드 중 하나를 이용해서 간편하게 생성 할 수 있다.


newCachedThreadPool(); : 이 메소드로 생성성된 스레드풀의 특징은 초기 스레드 갯수와 코어수는 0 이고, 스레드 갯수가 작업 갯수보다 적으면 스레드를 새로 생성한다. 물론 최대 스레드 갯수만큼만 생성하며 이는 실행 환경에 따라다르다. 생성된 스레드가 60초동안 아무일도 하지 않으면 스레드가 종료된다.


newFixedThreadPool(int nThreads); : 이 메소드로 생성한 스레드풀은 코어 갯수는 nThread개이며 nThread 갯수만큼 스레드가 생성된다. 여기서 생성된 스레드는 아무일도 하지 않아도 종료되지 않는다.


위의 두 메소드를 사용하지 않고, 직접 코어스레드 갯수와 최대 스레드 갯수를 설정하고 싶다면 ThreadPoolExcutor 객체를 생성하면 된다.



- 스레드풀 종료


스레드풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 main 스레드가 종료 되더라도 작업을 처리하기 위해 계속 실행 상태로 남아있으며, 따라서 프로세스가 종료되지 않는다. 다음 세개의 메소드들을 사용해서 종료 해 주어야 한다.


void shutDown(): 작업 큐에 있는 모든 작업을 처리한 후에 스레드풀을 종료시킨다.


List<Runnable> shutDownNow(): 현재 작업중인 스레드를 interrupt 해서 작업중지를 시도하고 스레드풀을 종료한다. 리턴값은 미처리된 작업들의 목록이다.


boolean awaitTermination(long timeout, TimeUnit unit): 모든 작업처리를 timeout 시간 내에 처리했으면 true, 아니면 작업중인 스레드를 interrupt() 하고 false를 리턴한다.


4. 작업 생성과 처리 요청


하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현한다. 둘의 차이는 작업처리 완료 후 리턴값이 있냐 없냐로 나뉘게 된다.


작업처리요청이란 ExcutorService의 작업큐에 Runnable 또는 Callable 객체를 넣는 행위를 말한다. ExcutorService 클래스는 이를 위해 두 종류의 메소드들을 제공한다.


void excute(Runnable command) : Runnable을 작업큐에 저장한다. 작업처리 결과를 받지 못한다.


Future<?> submit(Runnable task), Future<V> submit(Runnable task, V result), Future<V> submit(Callable<V> task) : 리턴된 Future을 통해 작업 처리 결과를 얻을 수 있음.


excute() 와 submit 메소드의 차이점은 크게 두가지 이다.


1) excute 는 작업처리 결과를 받지 못하고, submit는 작업처리를 받는다.


2) excute 는 작업 처리 도중 예외가 발생하면 스레드가 종료되고 해당 스레드는 스레드 풀에서 제거된다. 따라서 스레드풀은 다른 작업 처리를 위해 새로운 스레드를 생성한다. 반면에 submit은 작업처리도중 예외가 발생하더라도 스레드는 종료되지 않고 다음 작업을 위해 재사용 된다. 그래서 가급적이면 스레드 생성 오버헤드를 줄이기 위해서 submit을 사용하는 것이 좋다.


5. 블로킹 방식의 작업완료통보


ExcutorService의 submit 메소드는 매개값으로 준 Runnable 또는 Callable 작업을 스레드 풀의 작업큐에 저장하고 즉시 Future 객체를 리턴한다.


Future 객체는 작업 결과가 아니라, 작업이 완료 될 때 까지 기다렸다가(지연 되었다가) 최종결과를 얻는데에 사용된다. Future의 get() 메소드를 호출하면 스레드가 작업을 완료 할 때까지 블로킹 되었다가 작업을 완료하면 처리 결과를 리턴한다. 이것이 블로킹을 사용하는 작업 완료 통보 방식이다.

'Advanced java' 카테고리의 다른 글

익명 클래스란?  (0) 2017.02.10