데몬(daemon) 스레드
데몬 스레드는 주 스레드의 작업을 돕는 보조 스레드이다. 보통 사용자가 직접 제어하지 않고 백그라운드에서 실행된다. 주 스레드가 종료되면 데몬 스레드는 자동으로 종료된다.
코딩 방법
주 스레드에서 setDaemon() 메서드를 호출하여 데몬을 설정한다. start() 호출하여 스레드가 실행되기 전에 setDaemon()을 호출해야 정상적으로 설정된다. isDaemoni() 메서드로 데몬여부를 확인할 수 있다.
아래 코드를 보자.
public class DaemonTh extends Thread{
void whileDaemonRunning(){
System.out.println("데몬스레드 실행중");
}
void ifInterrupted(){
System.out.println("데몬스레드 interrupted");
}
void ifDaemonGone(){
System.out.println("데몬스레드 종료");
}
@Override
public void run() {
while(true){
try{
Thread.sleep(200);
whileDaemonRunning();
} catch (InterruptedException ie){
ifInterrupted();
ie.getMessage();
break;
}
}
ifDaemonGone();
}
}
먼저 interrupt() 메서드로 데몬 스레드를 중지시켜보자
public class Execute {
public static void main(String[] args) {
DaemonTh dm = new DaemonTh();
dm.setDaemon(true);
dm.start();
System.out.println("데몬여부 : "+ dm.isDaemon());
try {
Thread.sleep(1000);
dm.interrupt();
} catch (InterruptedException ie){
ie.getMessage();
}
System.out.println("end");
}
}
실행 결과
-데몬스레드가 중지되고 이어서 데몬스레드가 종료된다.
데몬여부 : true 데몬스레드 실행중 데몬스레드 실행중 데몬스레드 실행중 데몬스레드 실행중 end 데몬스레드 interrupted 데몬스레드 종료 |
이번에는 interrupt() 코드를 주석처리해 실행해보자
public class Execute {
public static void main(String[] args) {
DaemonTh dm = new DaemonTh();
dm.setDaemon(true);
dm.start();
System.out.println("데몬여부 : "+ dm.isDaemon());
try {
Thread.sleep(1000);
// dm.interrupt();
} catch (InterruptedException ie){
ie.getMessage();
}
System.out.println("end");
}
}
실행 결과
-데몬스레드가 중지되는 코드는 출력되지 않는다
-대신 메인스레드가 종료되었기 때문에 자동으로 데몬스레드도 실행되지 않는다
데몬여부 : true 데몬스레드 실행중 데몬스레드 실행중 데몬스레드 실행중 데몬스레드 실행중 end 데몬스레드 실행중 |
스레드 그룹(ThreadGroup)
스레드 그룹은 유사한 기능을 수행하는 스레드를 한데 묶어 관리할 때 사용한다. 스레드는 스레드 그룹에 포함되는데, 그룹을 명시적으로 선언하지 않는다면 자동으로 main 스레드에 속하게 된다.
스레드 그룹을 활용하면 스레드가 여러개인 경우 개별적으로 처리하는 대신 일괄적으로 처리할 수 있게 된다.
아래는 스레드그룹을 생성하고 스레드를 일괄적으로 관리하는 코드이다.
public class Employee extends Thread{
//생성자 인자값으로 스레드 그룹 생성하기
Employee(ThreadGroup threadGroup, String threadName){
super(threadGroup, threadName);
}
@Override
public void run() {
while (true){ //무한반복
try {
//interrupt() 메서드는 일시정지 상태에서 호출되기 때문에
//일부러 sleep 호출
Thread.sleep(100);
} catch(InterruptedException ie){
System.out.println(getName() + " is interrupted");
ie.getMessage();
break;
}
}
}
}
list() 호출하여 스레드 그룹에 대한 정보를 확인할 수 있다.
스레드 그룹에 대해 interrupt() 호출시 그룹에 속한 모든 스레드가 종료된다.
public class Execute {
public static void main(String[] args) {
ThreadGroup teamAlpha = new ThreadGroup("groupA");
ThreadGroup teamDeci = new ThreadGroup("groupB");
Employee empA = new Employee(teamAlpha, "홍길동");
Employee empB = new Employee(teamAlpha, "이순신");
Employee empC = new Employee(teamAlpha, "김유신");
Employee emp1 = new Employee(teamDeci, "120");
Employee emp2 = new Employee(teamDeci, "119");
empA.start();
empB.start();
empC.start();
emp1.start();
emp2.start();
ThreadGroup company = Thread.currentThread().getThreadGroup();
//현재 스레드 그룹의 이름과 최대우선순위 정보를 출력하는 메서드
company.list();
String alphaName = teamAlpha.getName();
String deciName = teamDeci.getName();
System.out.println("--teamAlpha.getName : "+ alphaName);
System.out.println("--teamDeci.getName : "+ deciName);
ThreadGroup threadGroup = teamAlpha.getParent();
System.out.println("--parentGroup : "+ threadGroup.getName());
//SecurityException 예외가 발생하지 않는다 -> 그룹변경 권한이 있음
teamAlpha.checkAccess();
//스레드 그룹의 interrupt() 호출해 중지시키기
teamAlpha.interrupt();
}
}
실행 결과
java.lang.ThreadGroup[name=main,maxpri=10] Thread[main,5,main] Thread[Monitor Ctrl-Break,5,main] java.lang.ThreadGroup[name=groupA,maxpri=10] Thread[홍길동,5,groupA] Thread[이순신,5,groupA] Thread[김유신,5,groupA] java.lang.ThreadGroup[name=groupB,maxpri=10] Thread[120,5,groupB] Thread[119,5,groupB] --teamAlpha.getName : groupA --teamDeci.getName : groupB --parentGroup : main 홍길동 is interrupted 이순신 is interrupted 김유신 is interrupted |
스레드 풀(Thread Pool)
스레드 풀은 제한된 개수만큼 스레드를 생성해 풀(pool)에 마련해두고, 작업요청시 풀에 대기중인 스레드가 작업을 맡아 처리하는 방식이다. 작업요청은 큐(queue) 방식, 즉 선입선출 방식으로 들어오게 된다. 스레드 풀 방식을 사용하면 작업 요청이 갑자기 많이 들어와도 스레드가 폭증하지 않기 때문에 서버가 마비되는 사태를 막을 수 있다.
예를들어 작업요청이 동시에 1,000개가 들어온다. 스레드 풀을 사용하지 않는 경우 스레드가 폭증하면서 시스템 자원이 들어온 작업처리에 집중된다. 시스템의 다른 활동에 제약이 발생할 수 있고, 이로 인해 시스템 성능이 저하될 수 있다.
반대로 스레드 풀에 스레드가 100개로 제한을 걸었다면, 동시에 처리할 수 있는 처리개수는 100개로 제한된다. 작업처리시간은 그만큼 길어질 수 있겠지만, 갑작스런 작업요청으로 시스템이 마비되는 사태는 예방할 수 있다.
쇼핑몰에서 계산줄이 길다고 해서 쇼핑몰 전직원이 계산대로 지원나가지 않는 것과 같다.
스레드풀 생성
스레드풀을 생성하는 방법은 3가지이다. newCashedThreadPool(), newFixedThreadPool() 는 메서드를 이용해 생성하는 방법이고, ThreadPoolExecutor 는 직접 객체를 생성해 스레드풀을 마련하는 방법이다. newCashedThreadPool()와 newFixedThreadPool() 도 내부적으로 ThreadPoolExecutor 객체를 생성하므로 사실상 동일한 방법이라고 볼 수 있다.
제공된 메서드를 호출하면 ExecutorService 객체가 생성되는데 이것이 스레드풀이다.
1. newCashedThreadPool 메서드로 생성하기
- 초기 스레드 개수와 코어 스레드 개수는 0
- 스레드 개수보다 작업개수가 많으면 새 스레드를 생성하여 작업을 처리
- 1개 이상의 스레드가 추가될 경우 60초간 스레드가 작업을 하지 않는 경우 추가된 스레드를 종료하고 풀에서 제거
- 작업이 끝나면 스레드를 제거하므로 스레드풀이 자원소비를 하지 않는다
ExecutorService service1 = Executors.newCachedThreadPool();
2. newFixedThreadPool 메서드로 생성하기
- 인자값으로 최대 코어스레드 개수를 할당
- 스레드 개수 보다 작업 개수가 많으면 새 스레드를 생성
- 인자값으로 준 개수 이상 스레드가 증가하지 않기 때문에, 최대 코어스레드 개수보다 작업이 많으면 작업이 대기 큐(queue)에 쌓인다
- 작업이 없더라도 생성한 스레드가 삭제되지 않는다
ExecutorService service2 = Executors.newFixedThreadPool(nThreads);
코어개수 값으로 Runtime.getRuntime().availableProcessors() 을 주면 시스템 프로세서(CPU코어) 개수만큼 스레드가 생성된다.
ExecutorService service3 = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
메서드 | 초기 스레드 개수 | 코어 스레드 개수 | 최대 스레드 개수 |
newCashedThreadPool | 0 | 0 | Integer.MAX_VALUE |
newFixedThreadPool(int nThreads) | 0 | nThreads | nThreads |
- 초기 스레드 : ExecutorService 객체가 생성되면서 기본적으로 생성되는 스레드 개수
- 코어 스레드 : 작업이 없을 경우 스레드풀에 남겨둘 스레드 개수
- 최대 스레드 : 스레드풀에 유지하는 스레드 최대 개수
3. ThreadPoolExecutor 객체로 생성하기
-인자값으로는 아래 내용을 할당하게 된다.
- int corePoolSize : 코어 스레드 개수
- int maximumPoolSize : 최대 스레드 개수
- long keepAliveTime : 스레드 미작업시 유지할 시간
- TimeUnit unit : 시간 단위
- BlockingQueue<Runnable> workQueue : 작업 큐
ExecutorService executorService = new ThreadPoolExecutor(
3, 100, 3, TimeUnit.MINUTES, new SynchronousQueue<>()
);
스레드풀 종료
메인 스레드가 종료되더라도 스레드풀의 스레드는 자동으로 종료되지 않는다. 때문에 명시적으로 스레드풀을 종료시키는 코드 작성이 필요하다.
메서드 | 내용 |
void shutdown() | 작업 큐에 남아있는 작업을 처리한 뒤에 안정적으로 종료 |
List<Runnable> shutdownNow() | -작업 큐에 남아있는 작업을 처리하지 않고 즉시 종료 -스레드를 interrupt하여 작업이 중지되므로 미처리된 작업목록을 리턴함 |
boolean awaitTermination(long timeout, TimeUnit unit) | -작업 마무리할 시간을 제공하는 방식 -시간내에 끝내면 true, 못 끝내면 false |
스레드풀 작업
1. 작업 생성
스레드 풀의 작업은 Runnable 인터페이스 및 Callable 인터페이스를 구현하여 생성한다. 두 인터페이스는 동일한 역할을 수행하며, 리턴값의 유무에만 차이가 있다. 작업이 들어오면 스레드풀의 스레드는 큐에서 처리할 작업(=Runnable 객체 혹은 Callable 객체)을 가져와서 각각 run(), call()을 수행한다.
Runnable 구현하여 작업생성하기
public class Test1 implements Runnable{
@Override
public void run() {
//스레드 처리
}
}
Callable 구현하여 작업생성하기
import java.util.concurrent.Callable;
public class Test2 implements Callable {
@Override
public Object call() throws Exception {
Object object = null;
//스레드 처리
return object;
}
}
2. 작업 처리
메서드 | 내용 |
void execute(Runnable command) | -Executor 인터페이스의 메서드 -Runnable을 작업큐에 저장함 -작업결과를 받지 못함 -작업도중 예외가 발생하면 스레드 종료, 해당 스레드는 스레드 풀에서 제거됨. 스레드풀은 새로운 스레드를 다시 생성함 (예외가 많을시 생성 오버헤드 발생 우려) |
<T> Future<T> submit(Callable<T> task) | -Runnable 또는 Callable을 작업큐에 저장함 -작업결과를 리턴함 -리턴타입 : Future 인터페이스 (java.util.concurrent) -작업도중 예외가 발생하더라도 해당 스레드를 제거하지 않고 재사용함 (예외가 많을시에도 오버헤드 우려 적음) |
Future<?> submit(Runnable task) | |
<T> Future<T> submit(Runnable task, T result) |
Future는 지연완료객체라고 하는 인터페이스이다. 작업결과를 담고 있고 있고, get() 메서드로 결과를 가져올 수 있다.
참고로 여기서 말하는 작업결과는 작업이 잘 수행되었는지에 대한 값을 말한다.
자세한 내용은 [3.작업 통보]에서 알아보자
아래 코드를 보자.
Runnable 에 작업을 부여하고, 스레드의 execute(), submit() 메서드로 실행하는 예제이다.
1) Runnable 구현 클래스
public class Todo implements Runnable {
@Override
public void run() {
try{
System.out.println(Thread.currentThread().getName()+ " | 작업");
Thread.sleep(200);
} catch (InterruptedException ie){
ie.getMessage();
}
}
}
2-1) execute() 메서드로 작업처리하는 코드
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Execute {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(4);
for(int i=0; i<8; i++){
threadPool.execute(new Todo());
}
threadPool.shutdown();
}
}
2-2) submit() 메서드로 작업처리하는 코드
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Execute {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(4);
for(int i=0; i<8; i++){
threadPool.submit(new Todo());
}
threadPool.shutdown();
}
}
실행 결과
pool-1-thread-1 | 작업 pool-1-thread-3 | 작업 pool-1-thread-4 | 작업 pool-1-thread-2 | 작업 pool-1-thread-1 | 작업 pool-1-thread-2 | 작업 pool-1-thread-3 | 작업 pool-1-thread-4 | 작업 |
아래 코드를 보자.
Runnable 에 일부러 예외를 발생시켜 execute() 와 submit() 의 스레드 생성여부 및 예외처리방식을 확인해보자.
1) Runnable 구현 객체 - 숫자로 변환시 예외(NumberFormatException)을 발생시키는 코드
public class Todo implements Runnable {
String st = null;
Todo(String st){
this.st = st;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+ " run 실행");
int changed = changeInteger(st);
System.out.println("리턴 : "+ changed);
}
//예외 발생시키는 코드
int changeInteger(String str){
return Integer.parseInt(str);
}
}
2-1) execute() 로 실행하기
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Execute {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(4);
for(int i=0; i<8; i++){
threadPool.execute(new Todo("홍길동"));
}
threadPool.shutdown();
}
}
실행 결과
... java.lang.NumberFormatException: For input string: "홍길동" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Integer.parseInt(Integer.java:580) at java.lang.Integer.parseInt(Integer.java:615) at thread.ex401_threadPool.ex02_스레드풀작업처리.test04_예외일으키기.Todo.changeInteger(Todo.java:20) at thread.ex401_threadPool.ex02_스레드풀작업처리.test04_예외일으키기.Todo.run(Todo.java:14) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) |
2-2) submit() 으로 실행하기
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Execute {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(4);
for(int i=0; i<16; i++){
threadPool.submit(new Todo("홍길동"));
}
threadPool.shutdown();
}
}
실행 결과
pool-1-thread-1 run 실행 pool-1-thread-4 run 실행 pool-1-thread-2 run 실행 pool-1-thread-3 run 실행 pool-1-thread-1 run 실행 pool-1-thread-1 run 실행 pool-1-thread-1 run 실행 pool-1-thread-2 run 실행 |
3. 작업완료 통보
스레드의 작업을 부여한 뒤에 잘 완료되었는지 확인할 필요가 있다. 작업스레드에 작업결과를 보고하라고 지정할 수 있는데 이를 통보라고 한다. 통보 방식은 아래와 같이 분류할 수 있다. 블로킹 방식은 다시 여러 방법으로 구분할 수 있다.
- 블로킹(blocking) 방식 : 작업완료될 때까지 블로킹(실행중지)되었다가 작업끝나면 결과를 알려주는 방식
- 리턴값이 없는 작업완료 통보
- 리턴값이 있는 작업완료 통보
- 작업결과를 외부 객체에 저장
- 콜백(callback) 방식 : 작업 후에 호출될 함수를 설정해 작업완료되면 자동으로 메서드가 실행되는 방식
-블로킹 방식은 작업이 요청된 뒤 완료될 때까지 실행중지된다.
-콜백 방식은 작업이 요청된 뒤 완료에 상관없이 다른 기능을 수행할 수 있다.
블로킹 방식 - 메서드
메서드 | 내용 |
Future<?> submit(Runnable task) | -Runnable 및 Callable을 작업 큐에 저장 -리턴된 Future 를 통해 작업처리 결과를 얻는다 |
Future<V> submit(Runnable task, V result) | |
Future<V> submit(Callable<V> task) |
Future<T> 인터페이스
submit() 메서드를 실행하면 Future 값이 반환된다. Future는 지연완료객체라고 한다. Future 객체에서 알아두어야 할 점은, Future 는 스레드 실행결과 값이 아니라는 점이다. 스레드가 정상적으로 실행되고 정상적으로 종료되었는지에 대한 객체이다.
Future 객체의 get() 메서드를 호출하면 스레드의 작업이 완료되기까지 기다렸다가 실행결과를 가져온다. 스레드 메서드 중 join()이 호출되면 작업이 종료될 때까지 기다리게 된다. 마치 join() 처럼, get()가 호출되면 작업완료까지 블로킹 되었다가 실행결과를 가져오게 된다. 이 때문에 get()을 처리하는 스레드는 작업완료까지 다른 작업을 수행할 수가 없다.
get() 결과는 아래와 같다
- 정상처리되면 null 리턴
- 작업 중 interrupt 되면 InterruptedException 예외발생
- 작업 중 예외가 발생하면 ExecutionException 예외발생
3-1-1) 블로킹 - 리턴값이 없는 작업완료 통보
스레드 인자값으로 Runnable 을 구현코드를 넣어준다. Runnable 은 리턴값이 없다.
아래 코드를 보자
숫자 인자값을 넣으면 그 제곱값을 계산하는 예제이다.
public class Test {
int num = 1;
Test(){
this(10);
}
Test(int num){
this.num = num;
}
Runnable runTodo = new Runnable() {
@Override
public void run() {
int answer = 0;
answer = num*num;
System.out.println("answer : "+ answer);
}
};
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(4);
Future future = service.submit(new Test(5).runTodo);
try{
future.get();
System.out.println("future.isDone() : "+ future.isDone());
} catch (InterruptedException ie){
ie.getMessage();
} catch (ExecutionException ee){
ee.getMessage();
} catch (Exception e){
e.getMessage();
}
service.shutdown();
}
}
실행 결과
answer : 25 future.isDone() : true |
3-1-2) 블로킹 - 리턴값이 있는 작업완료 통보
스레드 인자값으로 Callable을 구현코드를 넣어준다. Callable 은 리턴값이 있다.
아래 코드를 보자
똑같이 숫자 인자값을 넣으면 그 제곱값을 계산하는 예제이다.
public class Test {
int num = 1;
Test(){
this(10);
}
Test(int num){
this.num = num;
}
Callable runTodo = new Callable() {
@Override
public Integer call() throws Exception {
int answer = 0;
answer = num*num;
return answer;
}
};
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(16);
Future<Integer> future = service.submit(new Test(5).runTodo);
try{
int sum = future.get();
System.out.println("sum : "+ sum);
} catch (InterruptedException ie){
ie.getMessage();
} catch (ExecutionException ee){
ee.getMessage();
} catch (Exception e){
e.getMessage();
}
service.shutdown();
}
}
실행 결과
sum : 25 |
3-1-3) 블로킹 - 작업처리결과 외부객체에 저장하기
스레드가 실행한 결과를 외부 공유객체에 저장하는 방식이다. 스레드에서 외부 공유객체에 접근할 수 있도록 코드를 구성해야한다.
아래 코드를 보자
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Test01 {
class Result{
int accumValue;
synchronized void addValue(int value){
accumValue += value;
}
}
class Task implements Runnable{
Result result;
Task(Result result){
this.result = result;
}
@Override
public void run() {
int sum = 0;
for(int i=1; i<=10; i++){
sum+=i;
}
result.addValue(sum);
}
}
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(4);
Test01 t = new Test01();
Result result = t.new Result();
Runnable task1 = t.new Task(result);
Runnable task2 = t.new Task(result);
Runnable task3 = t.new Task(result);
Runnable task4 = t.new Task(result);
Runnable task5 = t.new Task(result);
Future<Result> future1 = service.submit(task1, result);
Future<Result> future2 = service.submit(task2, result);
Future<Result> future3 = service.submit(task3, result);
Future<Result> future4 = service.submit(task4, result);
Future<Result> future5 = service.submit(task5, result);
try{
result = future1.get();
result = future2.get();
result = future3.get();
result = future4.get();
result = future5.get();
} catch (InterruptedException ie){
ie.getMessage();
} catch (ExecutionException ee){
ee.getMessage();
} catch (Exception e){
e.getMessage();
}
System.out.println("처리결과 : "+ result.accumValue);
service.shutdown();
}
}
3-2) 콜백
콜백은 스레드가 요청된 작업을 완료하면 특정 메서드를 자동으로 실행하는 방식이다. 자동실행되는 메서드를 콜백 메서드라고 한다.
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CallbackEx {
private ExecutorService executorService;
public CallbackEx(){
executorService = Executors.newFixedThreadPool(8);
}
private CompletionHandler<Integer, Void> callback =
new CompletionHandler<Integer, Void>() {
@Override
public void completed(Integer result, Void attachment) {
System.out.println("completed - "+ result);
}
@Override
public void failed(Throwable exc, Void attachment) {
System.out.println("failed - "+ exc.toString());
}
};
public void doWork(final String x, final String y){
Runnable task = new Runnable() {
@Override
public void run() {
try{
int intX = Integer.parseInt(x);
int intY = Integer.parseInt(y);
int result = intX + intY;
callback.completed(result, null);
} catch (NumberFormatException nf){
callback.failed(nf, null);
} catch (Exception e){
e.getMessage();
}
}
};
executorService.submit(task);
}
public void finish(){
executorService.shutdown();
boolean isShut = executorService.isShutdown();
System.out.println("executorService 종료여부 : "+ isShut);
}
public static void main(String[] args) {
CallbackEx instance = new CallbackEx();
// instance.doWork("3", "5");
instance.doWork("7", "홍길동");
try {
Thread.sleep(1000);
} catch (InterruptedException ie){
ie.getMessage();
}
instance.finish();
}
}
'Java > 기본' 카테고리의 다른 글
[Java] 스트림(Stream) 익히기 (0) | 2021.07.17 |
---|---|
[Java] 람다식(Lambda) 익히기 (0) | 2021.07.16 |
[Java] 스레드(Thread) 제어하기 - 우선순위 설정, 동기화, 메서드 사용하기 (0) | 2021.07.08 |
[Java] 스레드(Thread) - 스레드 개념 및 생성하기 (0) | 2021.07.07 |
[Java] 타입문제예방하기102 - 열거형(enum)이란 (0) | 2021.07.01 |