Language/Java

Java - 멀티 스레드 정리

둉이 2021. 7. 2. 19:38

 

스레드란?
CPU가 독립적으로 처리하는 하나의 작업 단위
프로세스를 이루는 작은 단위

 

멀티 스레드

- 어플리케이션 내부에서의 멀티 태스킹

- 멀티 스레드가 모여 프로세스를 이루고 프로세스가 모여 멀티 프로세스를 이룸

 

메인 스레드

- main() 메소드의 첫 코드부터 순차적으로 실행

- 필요에 따라 작업 스레드들을 만들어 병렬로 코드 실행 가능(=멀티 스레드)

 

싱글 스레드 어플리케이션 vs 멀티 스레드 어플리케이션

싱글 스레드 프로세스와 멀티 스레드 프로세스의 차이점

- 싱글 스레드 어플리케이션

  : 메인 스레드가 종료되면 프로세스도 종료

- 멀티 스레드 어플리케이션

  : 실행 중인 작업 스레드가 남아있으면 종료되지 않음

 

작업 스레드

- 생성

1. Thread 클래스로부터 직접 생성(Runnable로 생성)

Thread thread = new Thread(new Runnable() {  // 혹은 Runnable 인터페이스를 구현한 객체 대입
   @Override
   public void run() {
      // 코드 내용
   }
});
// 혹은
Thread thread = new Thread(() -> {
   // 코드 내용
});

2. Runnable로 만들지 않고 Thread의 하위 클래스로부터 생성

Thread thread = new Thread() {  // 혹은 Thread 클래스를 상속한 객체 대입
   @Override
   public void run() {
      // 코드 내용
   }
};

- 실행

thread.start();

- 스레드 이름

thread.setName(설정할 이름);  // 스레드 이름 설정
thread.getName();  // 스레드 이름 가져오기

- 스레드 실행 상태

thread.getState();
// State.NEW, State.TERMINATED, State.RUNNABLE, State.BLOCKED, State.WAITING 등 반환

- 현재 실행하는 스레드 얻기

Thread thread = Thread.currentThread();

 

스레드 스케줄링

: 동시성, 병렬성

 스레드 스케줄링에 의해 스레드들은 아주 짧은 시간을 번갈아가면서 각자의 run() 메소드를 실행

 우선순위 방식과 라운드 로빈 방식을 함께 사용

- 우선순위 방식

  : 1~10, default=5

    setPriority() 메소드를 이용하여 우선순위 변경

// 우선순위 지정 예시
thread.setPriority(Thread.MAX_PRIORITY);  // 10
thread.setPriority(Thread.NORM_PRIORITY);  .. 5
thread.setPriority(Thread.MIN_PRIORITY);  // 1

- 라운드 로빈 방식

  : 시간 할당량(Time Slice)을 정해서 그 시간만큼 각자 실행

   우선순위가 같은 스레드의 병렬 실행에서 사용

 

공유 객체 사용 시 주의할 점

: 멀티 스레드 프로그램에서 스레드들이 객체를 공유하여 작업하는 경우, 중간에 어떤 스레드에 의해 상태가 변경된다면 다른 스레드들이 잘못된 값을 참조하는 경우 발생

스레드 2가 변경한 필드값을 변경 이후 스레드 1이 출력하면 변경된 필드값이 출력

  → 사용 중인 객체는 다른 스레드가 변경할 수 없도록 임계 영역을 선언해야 함

 

- 임계 영역

  : synchronized를 붙여서 선언

// 1. 동기화 메소드 선언
public synchronized void method() {  // 한 스레드가 동기화 메소드 사용시 다른 메소드는 종료될 때까지 사용 불가
   ...
   // 2. 동기화 블록 선언
   synchronized() {
      ...
   }
   ...
}

임계영역을 설정하여 올바른 공유 객체 값 출력

 

 - 동기화 메소드와 동기화 블록이 여러 개 있을 경우, 한 스레드가 그 중 하나만 실행중이어도 다른 스레드는 모든 동기화 메소드와 블록에 접근할 수 없음

 

스레드 객체

  1. 생성

  : 스레드 객체가 생성되고 start() 메소드가 호출되지 않은 상태

 

  2. 대기

  : start() 메소드 호출 후 자기 차례가 올 때까지 대기

 

  3. 실행

  : 스레드 스케줄링으로 선택된 스레드가 CPU를 점유하고 run() 메소드 실행

 

  4. 일시정지

  : 스레드가 실행될 수 없는 상태, 대기↔실행 구간에서 발생

 

  5. 종료

  : 코드 실행이 완료되어 종료

 

스레드 상태 제어

: 일시 정지, 종료

메소드 설명
interrupt() 일시 정지 상태의 스레드에서 인터럽트 예외를 발생시킴
catch에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있게 하는 메소드
notify()
notifyAll()
동기화 블록 내에서 wait() 메소드에 의해 일시 정지 상태가 된 스레드를 실행 대기 상태로 변환
sleep(long mills)
sleep(long mills, int nanos)
주어진 시간 동안 호출한 스레드(자기 자신)를 일시 정지
해당 시간 이후에는 실행 대기 상태가 됨
join()
join(long mills)
join(long mills, int nanos)
호출한 스레드가 종료될 때까지 다른 모든 스레드를 일시 정지
1. 매개값이 주어진 경우, 해당 시간이 지나면 자동적으로 실행 대기 상태가 됨
2. 매개값이 주어지지 않은 경우, 해당 스레드가 종료괴면 실행 대기 상태가 됨
wait()
wait(long mills)
wait(long mills, int nanos)
동기화 블록 내에서 스레드를 일시 정지
1. 매개값이 주어진 경우, 해당 시간이 지나면 자동적으로 실행 대기 상태가 됨
2. 매개값이 주어지지 않은 경우, notify()나 notifyAll() 메소드를 실행하여 실행 대기 상태가 됨
yield() 우선순위가 동일한 다른 스레드에게 실행을 양보하고 실행 대기
stop() 스레드 즉시 종료

1. sleep()

: 주어진 시간동안 일시정지

 

2. yield()

: 다른 스레드에게 실행 양보

 

3. join()

: 다른 스레드의 종료를 기다림

 

4. wait() ↔ notify(), notifyAll()

: 스레드 간 협업(두 개 이상의 스레드 실행 시)

스레드 종료

: run() 메소드가 모두 실행되면 자동으로 스레드 종료

  → 스레드를 즉시 종료하려면 플래그 변수를 이용하거나 interrupt() 메소드를 이용하여 예외처리를 하는 방법이 있음

// 스레드 인터럽트 발생 예시
public void run() {
   try {
      while(true) {
         sout("실행중");
         Thread.sleep(1);  // interrupt()를 사용하기 위해서는 스레드가 일시정지 상태여야 하므로 sleep(1) 사용
      }
   } catch(InterruptedException e) { ... }
}

- 인터럽트 메소드 호출 여부

boolean status = Thread.interrupted();  // 현재 스레드 기준
// 혹은
boolean status = objThread.isInterrupted();  // 지정한 스레드 기준

 

데몬(daemon) 스레드

: 주 스레드의 작업을 돕는 보조 스레드, 주 스레드가 종료되면 같이 종료

  스레드를 데몬으로 만들기 위해서는 데몬이 될 스레드의 setDaemon(true) 메소드를 호출하면 됨

main() {
   ...
   thread.setDaemon(true);  // start() 메소드가 실행되기 전에 setDaemon()이 실행되어야 함
   thread.start();
}

 

스레드 그룹

: 관련된 스레드를 묶어서 관리할 목적으로 이용

- 장점: 그룹 내에 포함된 모든 스레드들을 일괄 interrupt 가능

 

- 스레드 그룹 이름 얻기

ThreadGroup group = Thread.currentThread.getThreadGroup();
String groupName = group.getName();

- 모든 스레드에 대한 정보 얻기

Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();  // Map 타입으로 리턴Set<Thread> Set<Thread> threads = map.keySet();  // 모든 스레드의 목록을 Set 타입으로 리턴

for(Thread t : threads) {  // 기타 스레드 정보 얻기
   System.out.println("이름: " + t.getName() + (t.isDaemon() ? "(데몬)" : "(주)"));
   System.out.println("소속그룹: " + t.getThreadGroup().getName());
}

- 스레드 그룹 생성

ThreadGroup tg = new ThreadGroup(그룹이름);
// 혹은
ThreadGroup tg = new ThreadGroup(부모 스레드 그룹, 그룹이름);

- 생성된 스레드 그룹 사용

Thread t = new Thread(ThreadCroup group, Runnable target);
Thread t = new Thread(ThreadCroup group, Runnable target, String name);
Thread t = new Thread(ThreadCroup group, Runnable target, String name, long stackSize);
Thread t = new Thread(ThreadCroup group, String name);

- 스레드 그룹 메소드

메소드 설명
int activeCount() 현재 그룹 및 하위 그룹에서 활동 중인 모든 스레드의 갯수 리턴
int activeGroupCount() 현재 그룹에서 활동 중인 하위 그룹의 수 리턴
void checkAccess() 현재 스레드가 스레드 그룹을 
void destroy() 현재 그룹 및 하위 그룹 삭제
boolean isDestroyed() 현재 그룹이 삭제되었는지 여부 리턴
int getMaxPriority() 현재 그룹에 포함된 스레드의 최대 우선순위 리턴
void setMaxPriority() 현재 그룹에 포함된 스레드의 최대 우선순위 설정
String getName() 현재 그룹의 이름 리턴
ThreadGroup getParent() 현재 그룹의 부모 그룹 리턴
boolean parentOf(ThreadGroup g) 현재 그룹이 매개값으로 지정된 스레드 그룹의 부모인지 여부 리턴
boolean isDaemon() 현재 그룹이 데몬 그룹인지 리턴
void setDaemon() 현재 그룹을 데몬 그룹으로 설정
void list() 현재 그룹에 포함된 스레드와 하위 그룹에 대한 정보 출력
void interrupt() 현재 그룹에 포함된 모든 스레드 interrupt

 

스레드풀(ThreadPool)

: 작업 처리에 사용되는 스레드를 제한된 갯수만큼 정해놓고 작업 큐에서 들어오는 작업들을 하나씩 스레드가 맡아 처리

 어플리케이션의 성능 저하 예방

  ExecutorService, Executors 클래스를 이용하여 생성

 

- 스레드풀의 동작 방식

1. 어플리케이션이 스레드풀에 작업 처리 요청

2. 각 스레드가 작업 큐에서 작업을 가져와 처리

3. 어플리케이션에 결과 전달

 

- 스레드풀 생성

: newCachedThreadPool() 혹은 newFixedThreadPool(스레드 개수) 메소드로 생성

// 1. newCachedThreadPool로 생성
ExecutorService executorService = Executors.newCachedThreadPool();

// 2. newFixedThreadPool로 생성
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

// 3. ThreadPoolExecutor로 생성 → 직접 스레드 갯수와 타임 리밋 설정 가능
ExecutorService threadPool = new ThreadPoolExecutor(
   3, 100, 120L, Timeunit.SECONDS, new SynchronousQueue<Runnable>()
   // 코어 스레드, 최대 스레드, 놀고 있는 시간, 놀고 있는 시간 단위
);

 

  1. newCachedThreadPool()

   : 초기 스레드 개수와 코어 스레드 개수는 0개

    스레드 개수보다 작업 개수가 많으면 새 스레드 생성

    스레드가 60초 동안 아무 작업도 하지 않으면 종료

 

  2. newFixedThreadPool(n)

   : 스레드 개수보다 작업 개수가 많으면 새 스레드 생성

    스레드가 아무 작업을 하지 않더라도 종료하지 않음

 

- 스레드풀 종료

: main 스레드가 종료되도 스레드풀은 실행 상태로 남아있으므로 어플리케이션을 완전히 종료하기 위해서는 따로 스레드풀을 종료해야 함

executorService.shutdown();  // 작업 큐에 있는 모든 작업들이 완료되면 종료
// 혹은
executorService.shutdownNow();  // 진행중인 작업을 중단하고 종료

 

작업

- 작업 생성: Runnable 또는 Callable 인터페이스로 구현된 클래스 사용

// Runnable 구현 클래스 예시
Runnable task = new Runnable() {
   @Override
   public void run() {
      ...
   }
}

// Callable 구현 클래스
Callable<T> task = new Callable<T> {
   @Override
   public T call() throws Exception {
      ...
      return T;
   }
}

: Runnable은 리턴값 X, Callable은 T를 리턴

 → 작업 큐에 Runnable 또는 Callable 객체를 넣어서 작업 요청

 

- 작업 처리 요청 메소드

1. execute()

   : Runnable 객체만 사용 가능, void

     작업 처리중 예외가 발생하면 스레드 종료 및 스레드 풀에서 제거 → 새로운 스레드 생성해서 사용

2. submit() → 권장

   : Runnable, Callable 객체 사용 가능, 작업 처리 결과를 포함한 객체(Future) 리턴

     작업 처리중 예외가 발생해도 스레드는 종료되지 않고 재사용  새로운 스레드 생성 X

 

Future 객체(=지연 완료 객체)

: submit() 메소드 사용시 반환되는 결과 객체

// 작업 생성
ExecutorService executorService = Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors());

// 작업 실행 및 결과 객체 저장
Future<Integer> future = executorService.submit(task);

// 결과 객체 사용
try {
   int result = future.get();
   sout("결과 = " + result);
}

- 사용되는 작업 클래스별 리턴 타입

1. submit(Runanble task)

   → null 반환(리턴값이 없으므로)

try {
   future.get();
} catch(InterruptedException e) {
   // 작업 처리 도중 스레드가 인터럽트 될 경우 실행
} catch(ExecuteException e) {
   // 작업 처리 도중 예외가 발생할 경우 실행
}

2. submit(Runanble task, Integer result)

   → int 타입 값 반환

3. submit(Callable<String> task)

   → String 타입 값 반환

 

- 메소드 목록

리턴 타입 메소드명 설명
V get()
get(long timeout, TimeUnit unit)
작업이 완료될 때까지 기다렸다가 처리 결과 V 리턴(블로킹)
timeout 변수 사용시 해당 시간 전에 작업이 완료되면 결과 V 리턴, 완료되지 않으면 TimeoutException 발생
boolean cancel(bool 변수) 진행중인 작업 취소
boolean isCancelled() 작업이 취소되었는지 여부
boolean isDone() 작업이 완료되었는지 여부

 

CompletionService

: 스레드풀에서 작업 처리가 완료된 작업을 가져올 수 있는 클래스

 작업 완료 순으로 통보될 수 있도록 poll(), take() 메소드 제공

// completionService 생성
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(executorService);

 

- 메소드 목록

리턴 타입 메소드명 설명
Future<V> poll()
poll(long timeout, TimeUnit unit)
완료된 작업의 Future를 가져옴
완료된 작업이 없으면 즉시 null 리턴
Future<V> take() 완료된 작업의 Future를 가져옴
완료된 작업이 없으면 작업이 완료될 때까지 대기
Future<V> submit(Callable<V> task)
submit(Runnable task, V result)
스레드풀에 작업 처리 요청

→ 처리 완료된 작업의 Future를 얻으려면 completionService.submit(); 메소드로 작업 처리 요청을 해야 함

 

- 예시

public class CompletionServiceExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(executorService);

        System.out.println("작업 처리 요청");
        for(int i = 0; i < 3; i++) {
            completionService.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    int sum = 0;
                    for (int i = 1; i <= 10; i++) {
                        sum += i;
                    }
                    return sum;
                }
            });
        }
        System.out.println("처리 완료된 작업 확인");
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Future<Integer> future = completionService.take();
                        int value = future.get();
                        System.out.println("처리 결과 = " + value);
                    } catch (Exception e) {
                        break;
                    }
                }
            }
        });

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            executorService.shutdown();
        }
    }
}

 

작업 완료 통보

: 콜백 방식블로킹 방식이 있음

 

- 블로킹 방식

  : 작업 처리를 요청한 후 작업이 완료될 때까지 블로킹, 콜백 방식은 작업 처리를 요청한 후 결과를 기다릴 필요 없이 다른 기능 수행 가능

 

- 콜백 방식

: 스레드가 작업을 완료하면 특정 메소드를 자동 실행하는 기법

  Runnable 구현 클래스를 작성할 때 CompletionHandler를 사용하여 구현 가능

private CompletionHandler<Integer, Void> callback = new CompletionHandler<Integer, Void>() {
   @Override public void completed(Integer result, Void attachment) {
      ...  // 작업을 정상적으로 처리했을 때 호출
   }

   @Override public void failed(Throwable exc, Void attachment) {
      ...  // 작업중 예외가 발생했을 경우 호출
   }
};
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);
         callback.completed(result, null);  // 작업을 정상적으로 처리했을 때 호출
      } catch (NumberFormatException e) {
         callback.failed(e, null);  // 작업중 예외가 발생했을 경우 호출
      }
   }
};
executorService.submit(task);}

'Language > Java' 카테고리의 다른 글

Java - 람다식  (0) 2021.07.18
Java - 제네릭  (0) 2021.07.07
Java - 기본 API 클래스 정리  (0) 2021.06.25
Java - 중첩 클래스와 중첩 인터페이스, 예외 처리 복습  (0) 2021.06.25
Java - 인터페이스 복습  (0) 2021.06.24