8 분 소요

멀티 스레드

프로세스와 스레드

프로세스

  • 운영체제에서 실행 중인 하나의 애플리케이션
  • 애플리케이션을 실행하면 운영체제로부터 실행에 필요한 메모리를 할당받아 애플리케이션의 코드를 실행하는 것
  • 멀티 프로세스는 독립적 ⇒ 하나의 프로세스에서 오류가 발생해도 다른 프로세스에 영향 없음

스레드

  • 한 가지 작업을 실행하기 위해 순차적으로 실행할 코드
  • 멀티 스레드 ⇒ 하나의 프로세스 내에서 생성. 다른 스레드에 영향
    • 예외 처리 잘 해야 함
    • 데이터 병렬 처리, 네트워크 통신, 서버 개발

메인 스레드

  • 모든 자바 애플리케이션은 메인 스레드가 main() 메소드를 실행하면서 시작
  • 멀티 스레드를 생성해서 멀티 태스킹 수행

작업 스레드 생성과 실행

  • 작업 스레드도 객체로 생성

java.lang.Thread 클래스를 직접 객체화해서 생성

  • Runnable을 매개값으로 갖는 생성자 호출
  • Thread thread = new Thread(Runnable target);
  • Runnable : 작업 스레드가 실행할 수 있는 코드를 갖고 있는 객체
  • Runnable 구현 객체를 생성 ⇒ 매개값으로 Thread 생성자 호출 ⇒ 작업 스레드 생성 ⇒ start() 메소드로 호출, 실행

Thread 상속해서 하위 클래스를 만들어서 생성

  • 작업 스레드가 실행할 작업을 Runnable로 만들지 않고 Thread 하위 클래스로 작업 스레드 정의
  • Thread 클래스 상속 ⇒ run 메소드 오버라이딩 ⇒ 작업 스레드 생성 ⇒ start() 메소드 호출, 실행

스레드 우선순위

  • 동시성 : 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가면서 실행
  • 병렬성 : 멀티 작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 것
  • 스레드 스케줄링
    • 우선순위
      • 우선순위가 높은 스레드가 실행 상태를 더 많이 가지도록 스케줄링
      • 스레드 객체에 우선순위 번호 부여 ⇒ 개발자가 코드로 제어 가능
      • 기본적으로 5의 우선순위 할당, 10이 제일 높음
      • setPriority() 메소드로 변경 가능
    • 순환 할당
      • 시간 할당량을 정해서 하나의 스레드를 정해진 시간만큼 실행하고 다시 다른 스레드 실행

동기화 메소드와 동기화 블록

공유 객체를 사용할 때 주의점

  • 객체를 공유해서 작업할 경우 결과값이 섞일 수 있음
    • 스레드 작업이 끝날 때까지 객체에 잠금을 걸어 다른 스레드가 사용할 수 없도록 해야 함
  • 임계 영역 : 단 하나의 스레드만 실행할 수 있는 코드 영역
    • 임계 영역을 지정하기 위해 동기화(synchronized) 메소드와 블록 제공
    • 인스턴스와 정적 메소드 어디에든 synchronized 붙여서 사용

        public synchronized void method() {
        	// 임계 영역, 단 하나의 스레드만 실행
        }
      
    • 일부분만 임계 영역으로 설정

        public synchronized void method() {
        	synchronized(공유객체){
        		// 임계 영역, 단 하나의 스레드만 실행
        	}
        	// 여러 스레드 실행 가능
        }
      

스레드 상태

  • getState() 메소드로 상태 확인
상태 열거 상수 설명
객체 생성 NEW 스레드 객체 생성. 아직 start() 메소드 호출되지 않은 상태
실행 대기 RUNNABLE 실행 상태로 언제든지 갈 수 있는 상태
일시 정지 WAITING 다른 스레드가 통지할 때까지 기다리는 상태
  TIME_WAITING 주어진 시간 동안 대기
  BLOCKED 사용하고자 하는 객체의 락이 풀릴 때까지 기다림
종료 TERMINATED 실행 마침

스레드 상태 제어

  • 스레드의 상태를 변경하는 것
  • 멀티 스레드 프로그램을 만들기 위해 정교한 스레드 상태 제어가 필요

    | 메소드 | 설명 | | — | — | | interrupt() | 일시 정지 상태의 스레드에서 InterruptedException 예외 발생시켜 예외 처리 코드에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 함 | | notify() notifyAll() | 동기화 블록 내에서 wait() 메소드에 의해 일시 정지 상태에 있는 스레드 실행 대기 상태로 변경 | | sleep(long millis) sleep(long millis, int nanos) | 주어진 시간 동안 스레드를 일시 정지. 주어진 시간 지나면 자동으로 실행 대기 상태로 변경 | | join() join(long millis) join(long millis, int nanos) | join() 메소드를 호출한 스레드는 일시 정지 상태. join() 메소드를 멤버로 가지는 스레드가 종료되거나, 매개값으로 주어진 시간이 지나면 실행 대기 상태로 변경 | | wait() wait(long millis) wait(long millis, int nanos) | 동기화(synchronized) 블록 내에서 스레드를 일시 정지 상태로 변경. 매개값으로 주어진 시간이 지나면 자동적으로 실행 대기 상태로 변경. 시간이 주어지지 않으면 notify(), notifyAll() 메소드에 의해 실행 대기 상태로 갈 수 있음 | | yield() | 실행 중에 우선순위가 동일한 다른 스레드에게 실행을 양보하고 실행 대기 상태로 변경 |

데몬 스레드

  • 주 스레드의 작업을 돕는 보조적인 역할을 수행
  • 주 스레드가 종료되면 강제적으로 자동 종료
  • 데몬이 될 스레드의 setDaemon(true) 호출
  • start() 메소드 호출 전에 setDaemon(true) 호출해야 함 ⇒ IllegalThreadStateException 발생

      public static void main(String[] args) {
      	AutoSaveThread thread = new AutoSaveThread();
      	thread.setDaemon(true);
      	thread.start();
      }
    
  • isDaemon() 메소드로 데몬 스레드인지 아닌지 구별

스레드 그룹

  • 관련된 스레드를 묶어서 관리할 목적으로 사용
  • 명시적으로 스레드 그룹에 포함시키지 않으면 자신을 생성한 스레드와 같은 스레드 그룹에 포함
    • 작업 스레드는 대부분 main 스레드가 생성하므로 기본적으로 main 스레드 그룹에 포함

스레드 그룹 이름 얻기

// 스레드 그룹 이름 얻기
ThreadGroup group = Thread.currentThread().getThreadGroup();
String groupName = group.getName();

// 프로세스 내에서 실행하는 모든 스레드에 대한 정보
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();

스레드 그룹 생성

ThreadGroup tg = new ThreadGroup(String name);
ThreadGroup tg = new ThreadGroup(ThreadGroup parent, String name);
  • 부모 스레드 그룹을 지정하지 않으면 현재 스레드가 속한 그룹의 하위 그룹으로 생성
  • 스레드 그룹에 스레드 포함시키기

      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, long stackSize);
      Thread t = new Thread(ThreadGroup group, String name);
        
      // target : Runnable 구현 객체
      // name : 스레드 이름
      // stackSize : 스레드에 할당한 stack 크기
    

스레드 그룹 일괄 처리

메소드 설명
activeCount() 현재 그룹 및 하위 그룹에서 활동중인 모든 스레드의 수 리턴
activeGroupCount() 현재 그룹에서 활동 중인 모든 하위 그룹의 수 리턴
checkAccess() 현재 스레드가 스레드 그룹을 변경할 권한이 있는지 체크. 권한이 없으면SecurityException 발생
destroy() 현재 그룹 및 하위 그룹 모두 삭제. 그룹 내 포함된 모든 스레드들이 종료 상태가 되어야 함
isDestroyed() 현재 그룹이 삭제되었는지 여부 리턴
getMaxPriority() 현재 그룹에 포함된 스레드가 가질 수 있는 최대 우선순위 리턴
setMaxPriority(int pri) 현재 그룹에 포함된 스레드가 가질 수 잇는 최대 우선순위 설정
getName() 현재 그룹의 이름 리턴
getParent() 현재 그룹의 부모 그룹 리턴
parentOf(ThreadGroup g) 현재 그룹이 매개값으로 지정한 스레드 그룹의 부모인지 여부 리턴
setDaemon(boolean daemon) 현재 그룹을 데몬 그룹으로 설정
list() 현재 그룹에 포홤된 스레드와 하위 그룹에 대한 정보 출력
interrupt() 현재 그룹에 포함된 모든 스레드들 interrupt

스레드 풀(ExecutorService)

  • 갑작스런 병렬 작업의 폭증으로 인한 스레드의 폭증을 막기 위해 사용
  • 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐에 들어오는 작업들을 하나씩 스레드가 맡아 처리

스레드풀 생성

메소드명(매개변수) 초기 스레드 수 코어 스레드 수 최대 스레드 수
newCachedThreadPool() 0 0 Integer.MAX_VALUE
newFixedThreadPool(int nThreads) 0 nThreads nThreads

스레드풀 종료

  • 스레드풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 main 스레드가 종료되도 계속 실행 상태로 유지
    • 애플리캐이션을 종료하기 위해 스레드풀을 종료시켜야 함

      메소드명(매개 변수) 설명
      shutdown() 현재 처리 중인 작업뿐만 아니라 작업 큐에 대기하고 있는 모든 작업을 처리한 뒤 스레드풀 종료
      shutdownNow() 현재 작업 처리 중인 스레드를 interrupt해서 작업 중지를 시도하고 스레드풀 종료. 리턴값은 작업 큐에 있는 미처리된 작업의 목록
      awaitTermination(long timeout, TimeUnit unit) showdown() 메소드 호출 이후, 모든 작업 처리를 timeout 시간내에 완료하면 true, 완료하지 못하면 작업 처리 중인 스레드 interrupt 하고 false 리턴

작업 생성

  • 하나의 작업은 Runnable or Callable 구현 클래스로 표현.

    | Runnable 구현 클래스(리턴값 X) | Callable 구현 클래스(리턴값 O) | | — | — | | Runnable task = new Runnable() { @Override public void run() { // 스레드가 처리할 작업 } } | Callable task = new Callable() { @Override public T call() throws Exception { // 스레드가 처리할 작업 return T; } } |

작업 처리 요청

  • ExecutorService의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위

    | 메소드(매개 변수) | 설명 | 특징 | | — | — | — | | execute(Runnable command) | - Runnable을 작업 큐에 저장

    • 작업 처리 결과를 받지 못함 | 작업 처리 도중 예외가 발생하면 스레드 종료. 해당 스레드 스레드풀에서 제거 ⇒ 다른 작업 처리를 위해 새로운 스레드 생성 | | submit(Runnable task) submit(Runnable task, V result) submit(Callable task) | - Runnable 또는 Callable 을 작업 큐에 저장
    • 리턴된 Future를 통해작업 처리 결과를 얻을 수 있음 예외 발생해도 스레드 종료되지 않고 다음 작업을 위해 재사용. 스레드 생성 오버헤더를 줄이기 위해 submit 사용

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

  • ExecutorService의 submit() 메소드는 매개값으로 준 Runnable 또는 Callable 작업을 스레드풀의 작업 큐에 저장하고 즉시 Future 객체 리턴
  • Future 객체
    • 작업 결과가 아니라 작업이 완료될 때까지 기다렸다가 최종 결과를 얻는 데 사용
    • Future의 get() 메소드를 호출하면 스레드가 작업을 완료할 때까지 블로킹되었다가 작업을 완료하면 처리 결과 리턴

      메소드 작업 처리 완료 후 리턴 타입 작업 처리 도중 예외 발생
      submit(Runnable task) future.get() → null future.get() → 예외발생
      submit(Runnable task, Integer result) future.get() → int 타입 값 future.get() → 예외발생
      submit(Callable task) future.get() → String 타입 값 future.get() → 예외발생
    • 작업을 처리하는 스레드가 작업을 완료하기 전까지 get() 메소드가 블로킹되므로 다른 코드를 실행할 수 없음
      • get() 메소드를 호출하는 스레드는 새로운 스레드이거나 스레드풀의 또 다른 스레드가 되어야 함

        | 새로운 스레드를 생성해서 호출 | 스레드풀의 스레드가 호출 | | — | — | | new Thread(new Runnable() { @Override public void run() { try { future.get(); } catch (Exception e) { e.printStackTrace(); } } }).start(); | executorService.submit(new Runnable() { @Override public void run() { try { future.get(); } catch (Exception e) { e.printStackTrace(); } } }).start(); |

    • get() 이외의 메소드

      메소드 설명
      cancel(boolean mayInterruptIfRunning) 작업 처리가 진행중일 경우 취소시킴
      isCancelled() 작업이 취소되었는지 여부
      isDone() 작업 처리가 완료되었는지 여부
  • 리턴값이 없는 작업 완료 통보
    • Runnable 객체
  • 리턴값이 있는 작업 완료 통보
    • Callable 객체
  • 작업 처리 결과를 외부 객체에 저장
    • 외부 result 객체에 작업 결과 저장 ⇒ 애플리케이션이 result 객체를 사용해서 어떤 작업을 진행할 수 있을 것
    • result 객체는 공유 객체가 되어, 두 개 이상의 스레드 작업을 취합할 목적으로 이용
    • 작업 객체는 Runnable 구현 클래스로 생성 ⇒ 외부 result 객체를 사용해야 하므로 생성자를 통해 Result 객체를 주입받도록 해야 함

        class Task implements Runnable {
        	Result result;
        	Task(Result result) { this.result = result; }
        	@Override
        	public void run() {
        		// 작업 코드
        		// 처리 결과 result 저장
        	}
        }
      
  • 작업 완료 순으로 통보

    메소드명(매개 변수) 설명
    poll() 완료된 작업의 Future 가져옴
    완료된 작업 없으면 즉시 null 리턴  
    poll(long timeout, TimeUnit unit) 완료된 작업의 Future 가져옴
    완료된 작업 없으면 Timeout까지 블로킹  
    take() 완료된 작업의 Future 가져옴
    완료된 작업 없으면 있을 때까지 블로킹  
    submit(Callbale(V) task) 스레드풀에 Callable 작업 처리 요청
    submit(Runnable task, V result) 스레드풀에 Runnable 작업 처리 요청
    • take() 메소드가 리턴하는 완료된 작업은 요청한 작업의 순서가 아님
  • 콜백 방식의 작업 완료 통보
    • 스레드가 작업을 완료하면 특정 메소드를 자동 실행하는 기법
    • 블로킹 방식은 작업 처리 요청 후 작업이 완료될때까지 블로킹, 콜백 방식은 작업 처리 요청 후 결과 기다리지 않고 다른 기능 수행 가능 ⇒ 작업 처리 완료되면 자동으로 콜백 메소드 실행
    • Runnable 구현 클래스 작성 시 콜백 기능 구현 가능