자바 스레드와 스레드풀

junto·2024년 8월 11일
0

java

목록 보기
1/1
post-thumbnail

프로세스와 스레드

  • 운영체제에서 실행 중인 하나의 애플리케이션을 프로세스(process)라고 한다.
  • 스레드(thread)는 하나의 실행 흐름을 말하며 모든 자바 애플리케이션은 메인 스레드(main thread)가 main() 메소드를 실행하며 시작한다.
public static void main(String[] args) { 	// main thread 실행
  ...
}

데몬 스레드와 유저 스레드

  • 자바 스레드는 크게 데몬 스레드와 유저 스레드로 나뉜다.
  • 데몬 스레드(Daemon Thread)는 JVM이 종료될 때 같이 종료. 일반적으로 백그라운드에서 실행되며, 메인 스레드와 사용자 스레드가 종료되면 자동으로 종료된다. 그 이유는 주 스레드의 보조 역할을 수행하는데 주 스레드가 종료되면 데몬 스레드의 존재 이유가 없어지기 때문이다.
  • 유저 스레드(User Thread)는 메인 스레드가 종료되더라도 독립적으로 실행 가능하고, 모든 사용자 스레드가 종료되지 않으면 JVM은 종료되지 않는다.
public static void main(String[] args) {
	Thread userTread =
			new Thread(
					() -> {
						try {
							Thread.sleep(3000);
							System.out.println("UserThread finished");
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					});

	System.out.println("MainThread finished");
}
// MainThread finished
// UserThread finished
public static void main(String[] args) {
	Thread daemonThread =
		new Thread(
			() -> {
				try {
					Thread.sleep(3000);
					System.out.println("DaemonThread finished");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			});

	daemonThread.setDaemon(true);
	daemonThread.start();

	System.out.println("MainThread finished");
}
// MainThread finished

멀티 스레드

  • 메인 작업 이외에 추가적인 병렬 작업의 수만큼 스레드를 생성할 수 있다.
  • java.lang.Thread 클래스를 직접 객체화해서 생성(runnable interface)해도 되고, Thread를 상속해서 하위 클래스를 만드는 방법도 가능하다.

1. Thread 상속

public class MultiThread {
  static class MyThread extends Thread{
    @Override
    public void run() {
      try {
        Thread.sleep(3000);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
      System.out.println("extended userThread is finished");
    }
  }

  public static void main(String[] args){
    Runnable runnable =
        new Runnable() {
          @Override
          public void run() {
            try {
              Thread.sleep(2000);
            } catch (InterruptedException e) {
              throw new RuntimeException(e);
            }
            System.out.println("runnable userThread is finished");
          }
        };

    Thread thread = new Thread(runnable);
    thread.start(); // implement ruunable interface

    MyThread myThread = new MyThread();
    myThread.start(); // extend Thread class

    System.out.println("mainThread is finished");
  }
}

2. Runnable 익명 객체로 사용

  • 위에서 본 것처럼 Runnable을 익명 객체(람다)로 사용할 수 있다.
Thread thread = new Thread(() -> {
	try {
		Thread.sleep(3000);
		System.out.println("UserThread finished");
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
});

동시성과 병렬성

  • 동시성이란 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아 가며 실행하는 성질을 말하며, 병렬성은 멀티 작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 성질. 싱클 코어 CPU를 이용한 멀티 스레드 작업은 병렬적으로 실행되는 것으로 보이지만, 사실은 번갈아 가며 실행되는 동시성 작업이다.

우선순위(priority) 방식과 순환 할당(Round-Robin) 방식

  • 자바 스레드 스케줄링은 우선순위 방식과 순환 할당 방식을 사용한다.
  • 우선순위 방식은 스레드 객체에 우선 순위 번호를 부여하는 방식으로 동작한다. 자바 코드로 설정가능하지만, 스레드 스케줄링은 JVM이 운영체제 기능을 활용하는 방식으로 동작하기에 운영체제마다 동작 방식이 달라질 수 있다.
myThread.setPriority(1); // 1 ~ 10값을 줌. 높을수록 실행 기회를 더 많이 가짐. 상수로 설정 권
public class PriorityThread {

  public static void main(String[] args){

    Thread thread = new Thread(() -> {
      for(int i = 0; i < 50; i++) {
        System.out.println("priority1 thread running.");
      }
    });
    Thread thread2 = new Thread(() -> {
      for(int i = 0; i < 50; i++) {
        System.out.println("priority10 thread running.");
      }
    });

    thread.setPriority(1);
    thread2.setPriority(10);

    thread.start();
    thread2.start();

    System.out.println("mainThread is finished");
  }
}

// 출력결과를 보면 thread1이 실행되다가 thread2가 실행되면 thread2를 우선적으로 처리하고 thread1이 마무리가 됌
// priority1 thread running.
// priority1 thread running.
// priority1 thread running.
// priority1 thread running.
// priority10 thread running.
// priority10 thread running.
// priority10 thread running.
// priority10 thread running.
...
// mainThread is finished
// priority1 thread running.
...
  • 순환 할당 방식은 JVM에 의해 관리되기에 직접 자바 코드로 제어할 수 없음

멀티스레드와 병행 제어

동기화 필요성과 synchronized 키워드

  • 멀티스레드 환경에서 스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸어 다른 스레드가 사용할 수 없도록 해야 한다.

1. 임계 영역

  • 멀티스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역(critical season)이라고 한다.

2. 동기화 메소드 및 블록

  • 자바에선 임계 영역을 지정하기 위해 동기화(synchonized) 메소드와 동기화 블록을 제공한다.
  • 동기화 메소드를 만드는 방법은 메소드 선언에 synchronized 키워드를 붙이면 된다. 인스턴스와 정적 메소드 어디든 붙일 수 있다.
public sysnchronized void method() {
	// 임계 영역 
}

public void method() {
	// 공유 영역
    
    synchronized(공유 객체) {
    	// 임계 영역
    }
    
    // 공유 영역
}
  • 동기화 블록 외부 코드들은 여러 스레드가 동시에 실행할 수 있지만, 동기화 블록 내부 코드는 한 번에 하나의 스레드만 실행 할 수 있다.
  • 동기화 메소드로 작성된 것을 동기화 블록으로도 만들 수 있다.
public synchronized void setMemory(int memory) {
	this.memory = memory;
    try {
    	Thread.sleep(2000);
    } catch (InterruptedException e) {}
    System.out.println(Thread.cuurentThread().getName() + " " + this.memory);
}

public void setMemory(int memory) {
	synchronized (this) {
    	this.memory = memory;
    	try {
    		Thread.sleep(2000);
    	} catch (InterruptedException e) {}
    	System.out.println(Thread.cuurentThread().getName() + " " + this.memory);
    }
}
  • 일반적으로 동기화 메서드는 메서드 전체에 락을 걸기 때문에 메서드 블록보다 오버헤드가 크다고 한다. 메서드 특정 부분이 락을 걸 필요가 없다면, 동기화 블록을 사용해 락을 거는 부분을 최소화하는 것이 좋다.

스레드 상태와 스레드 상태 제어

1. 스레드 상태

  • NEW(생성 및 대기): 스레드가 생성되었지만 아직 실행되지 않은 상태. 스레드 객체를 생성하고, start() 메소드를 호출하면 스레드가 실행되는 것처럼 보이지만 사실은 실행 되기 상태로 스케줄링이 되지 않아 실행을 기다리고 있다.
  • RUNNING(실행): 실행 대기 상태에 있는 스레드 중 스레드 스케줄링으로 선택된 스레드가 CPU를 점유하고 run() 메소드를 실행하여 Running 상태가 된다.
  • TERMINATED(종료): 실행 상테에서 run() 메소드가 종료되면, 더 이상 실행할 코드가 없기에 스레드 실행은 멈추고 스레드는 TERMINATED 상태가 된다.
  • 일시 정지 상태
    - WAITING: 다른 스레드의 작업이 완료되기를 기다리는 상태
    • TIME_WATING: 주어진 시간 동안 기다리는 상태
    • BLOCKED: 락이 풀리기를 기다리는 상태

2. 스레드 상태 제어 메서드

1) 주어진 시간 동안 일시 정지(sleep)

  • 실행 중인 스레드를 일정 시간 동안 멈추게 하고 싶다면 Trhead 클래스의 정적 메소드인 sleep()을 사용한다.
try {
	Thread.sleep(1000);
} catch (InterruptedException e) {
	interrupt(1000); 
}
  • 매개 값에는 얼마 동안 일시 정지 상태로 있을지 밀리세컨드 단위로 시간을 준다.

2) 다른 스레드에 실행 양보(yield)

  • 아래 코드처럼 스레드가 특정 작업을 반복하여 실행할 때 무의미한 작업을 하는 경우가 있다. (BusyWaiting) 이때 다른 스레드에 CPU를 양보하면 CPU를 좀 더 효율적으로 사용할 수 있다.
public void run() {
	while (true) {
    	if (work) {
        	System.out.println("TrheadA 작업 내용");
        } else {
        	Thread.yield();
        }
    }
}
  • yield() 메소드를 호출한 스레드는 실행 대기 상태로 돌아가고 동일한 우선순위 또는 높은 우선순위를 갖는 다른 스레드가 실행 기회를 가질 수 있도록 한다.

3) 다른 스레드 종료 기다림(join)

  • 스레드는 다른 스레드와 독립적으로 실행하는 것이 기본이지만 다른 스레드가 종료될 때까지 기다렸다가 실행해야 하는 경우가 발생할 수 있다. 예를 들어 모든 계산 결과값을 받아 합계를 계산하는 스레드를 생각해 보자. 다른 스레드의 모든 계산 결과값을 기다리기 위해 Thread.join() 메서드를 사용한다.
public class JoinExample {
	public static void main(String[] args) {
    	SumThread sumThread = new SumThread();
        sumThread.start();
        
        try {
        	sumThread.join();
		} catch (InterruptedException e) {}
    }
}

4) 스레드 간 협업(wait, notify, notifyAll)

  • 두 개의 스레드를 교대로 번갈아가며 실행하고 싶을 때 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고, 자신은 일시 정지 상태로 만드는 것이다.
  • 공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메소드로 구분한다. 한 스레드가 작업을 완료하면 notify() 메소드를 호출해서 다른 스레드를 일시정지에서 실행 대기 상태로 만들고, 자신은 wait() 메소드를 호출해서 일시 정지 상태로 만든다.
public class WorkObject {
	public synchronized void methodA() {
    	notify();
        try {
        	wait();
        } catch (InterruptedExcpetion e) {}
    }
    
    public synchronized void methodB() {
    	notifiy();
        try {
        	wait();
        } catch (InterruptedException e) {}
    }
}
public class ThreadA extends Thread {
	private WorkObject workObject;
    
    // 공유 객체를 매개값으로 받아 필드에 저장
    public ThreadA(WorkObject workObject) {
    	this.workObject = workObject; 
    }
    
    @Override
    public void run() {
    	workObject.methodA();
    }
}
public class ThreadB extends Thread { ... }

public static void main(String[] args) {
	WorkObject sharedObject = new WorkObject();
    
    ThreadA threadA = new ThreadA(sharedObject);
    TrehadB threadB = new TrehadB(sharedObject);
    
    threadA.start();
    threadB.start();
    // 번갈아 가며 실행
}

5) 스레드 안전한 종료(stop, interrupt)

  • 스레드는 자신의 run() 메소드가 모두 실행되면 자동으로 종료되지만, 필요에 따라 실행 중인 스레드를 즉시 종료할 필요가 있다.
  • Thread를 즉시 종료시키기 위해 stop() 메서드를 제공하는데, 이 메소드는 스레드가 사용 중인 자원을 불안정한 상태로 남겨두기 때문에 deprecated 되었다.
  • stop flag를 이용해 run() 메소드의 종료를 유도하는 방법을 사용하거나 앞에서 본 interrupt() 메서드를 이용해 스레드가 일시 정지 상태에서 InterruptedException()을 발생시켜 정상 종료할 수 있다.

wai(), notify(), notifyAll()은 Object 클래스의 메소드이고, 그 이외는 Thread 클래스의 메소드들이다.

스레드 그룹

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

1. 그룹 이름 조회

TrheadGroup group = Thread.currentThread().getThreadGroup();
String groupName = group.getName();
  • Thread의 정적 메소드인 getAllStackTraces()를 이용하면 프로세스 내 실행하는 모든 스레드에 대한 정보를 알 수 있다.
Map<Trehad, StackTraceElement[]> map = Thread.getAllStackTraces();
  • 키는 스레드 객체이고, 값은 스레드 상태 기록이 갖고 있는 StackTraceElement 배열이다.

2. 그룹 생성

  • 명시적으로 스레드 그룹을 만들고 싶다면 다음 생성자 중 하나를 이용해서 TrheadGroup 객체를 만든다. TrheadGroup 이름을 주거나, 부모 ThreadGroup과 이름을 매개값으로 줄 수 있다.
ThreadGroup tg = new ThreadGroup(string name);
ThreadGroup tg = new ThreadGroup(ThreadGroup parent, string name);
  • 새로운 스레드 그룹을 생성한 후 이 그룹에 스레드를 포함하려면 Thread 객체를 생성할 대 생성자 매개값으로 스레드 그룹을 지정하면 된다.
Thread t = new Thread(ThreadGroup group, Runnable target);
Thread t = new Thread(ThreadGroup group, Runnable target, String name);
Thread t = new Thread(ThreadGroup group, Runnable target, String name, long stackSize);
Thread t = new Thread(ThreadGroup group, String name);

3. 그룹 일괄 인터럽트

  • 여러 스레드를 하나의 그룹에 포함하면 어떤 이점이 있을까? 스레드 그룹에서 제공하는 interrupt() 메서드를 이용하면 그룹 내에 포함된 모든 스레드들을 일괄 interrupt()할 수 있다.
  • 이외에도 제공하는 여러 메서드가 있다.
    • activeCount(): 현재 그룹 및 하위 글춥에서 활동 중인 모든 스레드 수 리턴
    • activeGroupCount(): 현재 그룹에서 활동 중인 모든 하위 그룹 수 리턴
    • checkAccess(): 현재 스레드가 스레드 그룹을 변경할 권한이 있는지 검사, 권한이 없으면 SecurityException을 발생
    • destroy(): 현재 그룹 및 하위 그룹을 모두 삭제. 단 그룹 내 포함된 모든 스레드가 종료 상태이어야 함는 최대 우선순위를 리턴

스레드 풀

  • 갑작스러운 병렬 작업의 폭증으로 인한 오버헤드를 피하려면 스레드풀(ThreadPool)을 사용해야 한다.

스레드 풀 필요성

  • 스레드 풀은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐에 들어오는 작업을 하나씩 스레드가 맡아 처리한다. 작업이 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리하기에 작업 처리 요청이 폭증되어도 스레드 전체 개수가 늘어나지 않아 애플리케이션 성능이 급격히 나빠지지 않게 된다.
  • 따라서 매번 요청마다 새로운 스레드를 생성하는 오버헤드를 피하기 위해 스레드 풀이 필요하다.

스레드 풀 장단점

  • 장점
    - 스레드 생성과 소멸에 드는 비용을 줄일 수 있다.
    - 스레드의 수를 제한하여 시스템 자원을 효율적으로 사용할 수 있다.
    - 요청마다 스레드를 생성하지 않기에 응답 시간이 빠르다.
  • 단점
    - 스레드 풀의 크기를 부적절하게 설정하면 스레드가 부족하거나 너무 많이 생성될 수 있다. 스레드가 부족하면 지연 시간이 길어지고, 스레드가 과도하게 생성되면 시스템 자원이 낭비된다.
    • 스레드 풀을 잘못 사용하면 특정 스레드가 서로 자원만을 요청하는 데드락 상태에 빠질 수 있다.

스레드 풀 사용 방법

1. 스레드 풀 생성

1) newCachedThreaedPool()

  • 초기 스레드 수: 0, 코어 스레드 수: 0, 최대 스레드 수: Integer.MAX_VALUE
  • 초기 스레드 수는 ExecutorService 객체가 생성될 때 기본적으로 생성되는 스레드 수를 말하며, 코어 스레드 수는 스레드 수가 증가한 후 사용되지 않는 스레드를 스레드풀에서 제거할 때 최소한 유지해야 할 스레드 수를 의미한다. 최대 스레드 수는 스레드 풀에서 관리하는 최대 스레드 개수이다.
  • 1개 이상의 스레드가 추가되었을 때 60초 동안 추가된 스레드가 아무 작업을 하지 않으면 추가된 스레드를 종료하고 풀에서 제거한다.
ExecutorService executorService = Executors.newCachedTrheadPool();

2) newFixedThreadPoll(int nThreads)

  • 초기 스레드 수: 0, 코어 스레드 수: nThreads, 최대 스레드 수: nThreads
  • 스레드 개수보다 작업 개수가 많으면 새 스레드를 생성시키고 작업을 처리한다. 최대 스레드 개수는 매개값으로 준 nTrheads이다. 스레드가 작업을 처리하지 않고 놀고 있더라도 스레드 개수가 줄지 않는다.
// CPU 코어 개수만큼 스레드 생성
ExecutorService executorService = Executors.newFixedThreadPool(
	Runtime.getRuntime().availableProcessors()
);

3) 직접 스레드풀 생성

  • ThreadPoolExecutor 객체를 생성하여 초기 스레드 개수 0개, 코어 스레드 개수 3개, 최대 스레드 개수가 100개인 스레드 풀을 생성해 보자. 그리고 코어 스레드 3개를 제외한 나머지 추가된 스레드가 120초 동안 놀고 있으면 해당 스레드를 제거하여 스레드 수를 관리한다.
ExecutorService ThreadPool = new ThreadPoolExecutor(
	3, // 코어 스레드 개수
    100, // 최대 스레드 개수
    120L, // 놀고 있는 시간
    TimeUnit.SECONDS, /// 놀고 있는 시간 단위
    new SynchronousQueue<Runnable> // 작업 큐
);

2. 스레드풀 종료

  • 스레드풀의 스레드는 데몬 스레드가 아니기에 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아있다.
  • ExecutorService는 종료와 관련해서 다음 3개 메서드를 제공한다.
    • shutdown(): 현재 처리 중인 작업뿐만 아니라 작업 큐에 대기하고 있는 모든 작업을 처리한 뒤에 스레드 풀을 종료
    • shutdownNow(): 현재 작업 처리 중인 스레드를 interrupt해서 작업 중지를 시도하고 스레드 풀을 종료시킨다. 리턴값은 작업 큐에 있는 미처리된 작업(Runnable)의 목록이다.
    • awaitTermination(long timeout, TimeUnit unit): shutdown() 메소드 호출 이후, 모든 작업 처리를 timeout 시간 내에 완료하면 true를 리턴하고, 완료하지 못하면 작업 처리 중인 스레드를 interrupt하고 false를 리턴한다.

일반적으로 남아있는 작업을 마무리하고 종료할 때는 shutdown(), 남아있는 작업과는 상관없이 강제로 종료할 때는 shutdownNow()를 호출한다.

3. 작업 생성과 처리 요청

1) 작업 생성

  • 하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현한다. 작업 처리 완료 후 리턴값 여부에 따라 사용하며 없다면 Runnable, 있다면 Callable을 사용한다.
Runnable task = new Runnable() {
	@Override
    public void run() {
    	// 작업
	}
}

Callbale<T> task = new Callable<T>() {
	@Override
    public T call() throws Exception {
    	// 작업
        return T;
    }
}

2) 작업 처리 요청

  • 작업 처리 요청이란 ExecutorService 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위를 말한다.
  • ExecutorService는 작업 처리 요청을 위해 다음 두 가지 메서드를 제공한다.
    • execute(Runnable command): Runnable을 작업 큐에 저장. 작업 처리 결과를 받지 못한다.
    • submit(Runnable task): Runnable 또는 Callble을 작업 큐에 저장. 리턴된 Future를 통해 작업 처리 결과를 얻을 수 있다.

execute()는 작업 처리 도중 예외가 발생하면 스레드가 종료되고 해당 스레드는 스레드 풀에서 제거되는 반면, submit()은 작업 처리 도중 예외가 발생하더라도 스레드는 종료되지 않고 다음 작업을 위해 재사용된다.

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

  • Future를 이용한 블로킹 박식의 작업 완료 통보에서 주의할 점은 작업을 처리하는 스레드가 작업을 완료하기 전까지는 get() 메소드가 블로킹 되므로 다른 코드를 실행할 수 없다.
  • Future 객체는 작업 결과를 얻기 위한 get() 메소드 외에도 다음과 같은 메소드를 제공한다.
    • caccel(boolean mayInterruptIfRunning): 작업 처리가 진행 중일 경우 취소
    • isCancelled(): 작업이 취소되었는지 여부
    • isDone(): 작업 처리가 완료되었는지 여부

1) 리턴값이 없을 때

  • 결과값이 없는 작업 처리 요청은 submit(Runnable task) 메소드를 이용한다. 결과값이 없음에도 Future 객체를 리턴하는 이유는 스레드가 작업 처리를 정상적으로 완료했는지, 아니면 작업 처리 도중 예외가 발생했는지 확인하기 위해서다.
Future future = executorService.submit(task);
  • 작업 처리가 정상적으로 완료되었다면 Future의 get() 메서드는 null을 리턴한다.

2) 리턴값이 있을 때

  • 스레드풀의 스레드가 작업을 완료한 후 처리 결과를 얻어야 한다면 작업 객체를 Callable로 생성한다.
Future<T> future = executorService.submit(task);

3) 작업 처리 결과를 외부 객체 저장

  • 스레드가 작업 처리를 완료하고 외부 Result 객체에 작업 결과를 젖아하면 애플리케이션이 Result 객체를 사용해서 특정 작업을 진행할 수 있을 것이다. 대개 Result 객체는 공유 객체가 되어 두 개 이상의 스레드 작업을 취합할 목적으로 사용한다. 이러한 작업을 위해 ExecutorService의 submit(Runnable task, V result) 메서드를 사용할 수 있는데, V가 바로 Result가 된다.
Result result = ...;
Runnable task = new Task(result);
Future<Result> future = executorService.submit(task, result);
result = future.get();

4) 작업 완료 순으로 통보

  • 작업 요청 순서대로 작업 처리가 완료된 것이 아니고, 작업의 양과 스레드 스케줄링에 따라 먼저 요청한 작업이 나중에 완료되는 경우도 발생한다.
  • 스레드 풀에서 작업 처리가 완료된 것만 통보받는 방법이 있다. CompletionService를 이용하면 처리 완료된 작업을 가져오는 poll()과 take() 메소드를 사용할 수 있다.
ExecutorService executorService = Executors.newFixedThreadPoll(
	Runtime.getRuntime().availableProcessors();
);

CompletionService<V> completionService = new ExecutorCompletionService<V>(
	executorService
);

completionService.submit(Callable<V> task);
completionService.submit(Runnable task, V result);

4. 콜백 방식 작업 완료 통보

  • 블로킹 방식은 작업 처리를 요청한 후 작업이 완료될 때까지 블로킹되지만, 콜백 방식은 작업 처리를 요청한 후 결과를 기다릴 필요 없이 다른 기능을 수행할 수 있다.
  • ExecutorService는 콜백을 위한 별도의 기능을 제공하진 않지만, Runnable 구현 클래스를 작성할 때 콜백 기능을 구현할 수 있다. 직접 정의할 수도 있고 java.nio.channelsCompletionHandler를 이용할 수도 있다.
CompletionHandler<V, A> callback = new CompletionHandler<V, A>() {
	@Override
    public void completed(V result, A attachment) {}
    
    @Override
    public void failed(Throwable exc, A attachment) {}
}
  • completed()는 작업을 정상 처리했을 때 호출되는 메서드이고, failed()는 작업 처리 도중 예외가 발생했을 때 호출되는 콜백 메소드이다.
  • CompletionHandler의 V 타입 파라미터는 결과값 타입이고, A는 첨부값 타입이다. 첨부값은 콜백 메소드에 결과값 이외에 추가적으로 전달하는 객체라고 생각하면 된다. 첨부 값이 필요 없다면 Void를 전달한다.
profile
꾸준하게

0개의 댓글