[10주차] 멀티쓰레드 프로그래밍

목차

  • Thread 클래스와 Runnable 인터페이스
  • 쓰레드의 상태
  • 쓰레드의 우선순위
  • Main 쓰레드
  • 동기화
  • 데드락

1. 쓰레드란?

1.1 프로세스

  • 실행 중인 프로그램

  • [자원 + 쓰레드]로 구성되어 있다.

1.2 쓰레드

  • 프로세스 내에서 실제 작업을 수행하는 일꾼이다.

  • 모든 프로세스는 최소한 하나의 쓰레드를 가지고 있다.

하나의 새로운 프로세스를 생성하는 것보다, 하나의 **새로운 쓰레드를 생성하는 것이 더 적은 비용**이 든다.

2. 멀티쓰레드

  • 쓰레드, 즉 일꾼이 여러개인 것이다.

  • 대부분의 프로그램이 멀티쓰레드로 작성되어 있다.

  • 싱글쓰레드

image

  • 멀티쓰레드

image

2.1 멀티쓰레드의 장점

  • 시스템 자원을 효율적으로 사용할 수 있다.

  • 사용자에 대한 응답성이 향상된다.

  • 작업이 분리되어 코드가 간결해진다.

2.2 멀티쓰레드의 단점

  • 동기화에 주의해야한다.

  • dead-lock(교착상태)가 발생하지 않도록 주의해야 한다.

  • 각 쓰레드가 효율적으로 고르게 실행될 수 있게 해야 한다.

  • 특정 쓰레드가 작업할 기회를 얻지 못하고 기아(굶어죽음)상태가 될 수 있음

3. 쓰레드의 I/O 블락킹

  • 싱글쓰레드 일때 외부장치의 입/출력 대기 등으로 작업이 중단되는 현상이다.
class ThreadIO {
  public static void main(String[] args) {
    // 입력받기 전까지 쓰레드의 I/O 블락킹 발생
    String input = JOptionPane.showInputDialog("아무 값이나 입력하세요");
    
    for(int i = 10; i > 0; i--) {
      try { Thread.sleep(1000) }
    }
  }
}

image

  • 이때, 멀티쓰레드로 프로그래밍하면 I/O 중단 시간 동안 다른 작업을 수행할 수 있기 때문에 효율적이다.
class ThreadIO {
  public static void main(String[] args) {
    ThreadIO thIO = new ThreadIO();
    thIO.start();
    
    String input = JOptionPane.showInputDialog("아무 값이나 입력하세요");
  }
}

class ThreadIO extends Thread {
  public void run() {
    for(int i = 10; i > 0; i--) {
      try { 
        sleep(1000) 
      } catch(Exception e) {}
    }
  }
}

4. Thread 클래스

  • java.lang.Thread

4.1 Thread 클래스 생성자

생성자 설명
Thread() Allocates a new Thread object.
Thread(Runnable target) Allocates a new Thread object.
Thread(Runnable target, String name) Allocates a new Thread object.
Thread(String name) Allocates a new Thread object.

4.2 Thread 클래스 필드

  • 쓰레드의 우선순위

  • 작업의 중요도에 따라 쓰레드의 우선순위를 다르게 하여 특정 쓰레드가 더 많은 작업시간을 할당받게할 수 있다. (순차적 실행 X)

  • 자바에서는 쓰레드의 우선순위를 1~10까지 보유한다.

필드 설명
public static final int MAX_PRIORITY = 10 최대 우선 순위
public static final int MIN_PRIORITY = 1 최대 우선 순위
public static final int NORM_PRIORITY = 5 보통 우선 순위
  • 디폴트 우선 순위는 5이다.

  • void setPriority(int newPriority) : 쓰레드의 우선순위를 지정한 값으로 변경한다.

  • int getPriority() : 쓰레드의 우선순위를 반환한다.

4.3 Thread 클래스 메소드

4. Thread 클래스와 Runnable 인터페이스

Thread 클래스를 상속해서 쓰레드를 구현하기

  • run() 메소드를 overriding해야 한다.
class MyThread1 extends Thread {
  // Thread 클래스의 run()을 오버라이딩
  public void run() {
    // 작업내용
    for(int i = 0; i < 5; i++) {
      System.out.println(this.getName()); // 조상인 Thread의 getName()을 호출
    }
  }
}
MyThread1 t1 = new MyThread1(); // 쓰레드의 생성
t1.start(); // 쓰레드 실행

Runnable 인터페이스

@FunctionalInterface
public interface Runnable {
  public abstract void run();
}
  • 함수형 인터페이스로 run()이라는 추상메소드 하나만 존재한다.

Runnable 인터페이스를 구현해서 쓰레드를 구현하기

class MyThread2 implements Runnable {
  // Runnable 인터페이스의 추상메서드 run()을 구현
  public void run() {
    // 작업내용
    for(int i = 0; i < 5; i++) {
      System.out.println(Thread.currentThread().getName()); // Thread.currentThread() : 현재 실행중인 쓰레드 반환
    }
  }
}
Runnable r = new MyThread2();
Thread t2 = new Thread(r);  // Thread(Runnable r) : run()이라는 메소드 구현체를 Runnable 인터페이스로 받는 것이다.
// Thread t2 = new Thread(new MyThread2());
t2.start();
Tip
  • 쓰레드를 구현하는 것은 run()의 몸통 {} 을 채우는 것이다.
  • Runnable는 인터페이스이기 때문에 다중 상속이 가능하다.

쓰레드의 상태

image

  • NEW : 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태

  • RUNNABLE : 실행 중 또는 실행 가능한 상태

  • BLOCKED : 동기화블럭에 의해서 일시정지된 상태 (Lock이 풀릴 때까지 기다리는 상태)

  • WAITING : 쓰레드의 작업이 종료되지는 않았지만 실행 가능하지 않은 (unrunnanle) 일시정지 상태

  • TIMED_WAITING : WAITING 상태에서 일시정지 시간이 지정된 경우

  • TERMINATED : 쓰레드의 작업이 종료된 상태

쓰레드의 실행제어 메소드

sleep() : 현재 쓰레드를 지정된 시간동안 멈추게 하기

  • 현재 쓰레드를 지정된 시간동안 멈추게 한다.

  • 자기 자신의 thread에 대한 제어만 가능한 static 메소드이다. (= yield())

  • 특정 쓰레드를 지정해서 멈추게 하는 것은 불가능하다.

  • static void sleep(long millis) : 1000분의 1초 단위의 매개변수를 넣어주면 해당 시간만큼 쓰레드가 멈춘다.

  • InterruptedException 예외처리가 필수이다. (InterruptedException는 Exception의 자손)

  • 매번 예외처리가 번거롭다면 아래와 같이 새로운 메소드를 만들어서 사용할 수도 있다.

void delay(long millis) {
  try {
    Thread.sleep(millis);
  } catch(InterruptedException e) {}
}

// 사용
delay(15); // Thread.sleep(1, 500000) + 예외처리
  • time up(시간 종료) 또는 interruped(깨우기) 되면 sleep 상태를 벗어난다.

interrupt() : 대기 상태 깨우기

  • 대기상태(WAITING)인 쓰레드를 실행대기 상태(RUNNABLE)로 만든다.

  • void interrupt() : 쓰레드의 interrupted 상태를 false에서 true로 변경한다.

  • boolean isInterrupted() : 쓰레드의 interrupted 상태를 반환한다.

  • static boolean interrupted() : 현재 쓰레드의 interrupted상태를 알려주고, false로 초기화한다.

  • 예제)

import javax.swing.*;

public class Ex_Interrupt {
    public static void main(String[] args) {
        Ex_Thread_1 th1 = new Ex_Thread_1();
        th1.start();
        System.out.println("입력전"+th1.isInterrupted()); // false

        String input = JOptionPane.showInputDialog("값 입력");
        System.out.println(input);
        th1.interrupt(); // interrupted는 true가 된다.

        System.out.println("interrupt 후/isInterrupted :"+th1.isInterrupted()); // true
        System.out.println("interrupt 후/interrupted :"+Thread.interrupted()); // false

    }
}
class Ex_Thread_1 extends Thread {
    public void run() {
        int i = 10;

        while (i!=0 && !isInterrupted()) {
            System.out.println(i--);
            for(long x = 0; x<2500000000L; x++); // 시간 지연
        }
        System.out.println("카운트가 종료되었습니다.");
    }
}

suspend() : 일시정지

  • dead-lock(교착상태)를 일으킬 위험이 있어 사용 권장 X

resume() : 일시정지 재개

  • 일시정지된 쓰레드를 실행대기 상태로 만든다.

  • dead-lock(교착상태)를 일으킬 위험이 있어 사용 권장 X

start() : 쓰레드의 실행

  • 쓰레드를 생성한 후에 start()를 호출해야 쓰레드가 작업을 시작한다.

  • OS스케줄러가 실행순서를 결정한다.

  • start() 가 새로운 호출스택을 생성해서 run()을 올린다. (올린 후, start()는 종료됨)

stop() : 쓰레드의 소멸

  • dead-lock(교착상태)를 일으킬 위험이 있어 사용 권장 X

join() : 작업이 끝날때까지 기다리기

  • 지정된 시간동안 특정 쓰레드가 작업하는 것을 기다린다.

  • void join() : 작업이 모두 끝날 때까지 기다린다.

  • void join(long millis) : 천분의 일초 동안 기다린다.

static long startTime = 0;

// main메서드 (main쓰레드가 수행)
publilc static void main(String args[]) {
    Thread1 th1 = new Thread1();
    Thread2 th2 = new Thread2();

    th1.start();
    th2.start();
    
    startTime = System.currentMillis(); // 시작시간

    try {
      th1.join();     // main 쓰레드가 th1의 작업이 끝날 때까지 기다린다.
      th2.join();     // main 쓰레드가 th2의 작업이 끝날 때까지 기다린다.
    } catch(InterruptedException e) {}
    
    System.out.print("소요시간" + (System.currentMillis() - startTime)); // 소요시간
}

// Thread1 쓰레드
class Thread1 extends Thread {
  public void run() {
      // 작업내용....
  }
}

// Thread2 쓰레드
class Thread2 extends Thread {
  public void run() {
      // 작업내용....
  }
}

yield() : 남은 시간을 양보하기

  • 남은 시간을 다음 쓰레드에게 양보하고, 자기(현재 쓰레드)는 실행대기한다.

  • yield()interrupt()를 적절히 사용하면 응답성과 효율을 높일 수 있다.

  • OS 스케줄러에게 통보하는 것 뿐이므로 지정한 시간만큼 정확히 작동하기를 기대해선 안된다.

wait()

notify()

Main 쓰레드

  • main 메서드의 코드를 수행하는 쓰레드이다.

  • 쓰레드는 1) 사용자 쓰레드, 2) 데몬 쓰레드(보조) 두 종류가 있는데 main쓰레드는 ‘사용자 쓰레드'이다.

  • 실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다. (main메소드의 종료 여부는 상관 X)

데몬쓰레드(Daemon Thread)

  • 일반쓰레드(non-daemon thread)의 작업을 돕는 보조적인 역할을 수행한다.

  • 일반 쓰레드가 종료되면 자동으로 종료된다.

  • 가비지 컬렉터, 자동저장, 화면 자동갱신 등에 사용된다.

  • 무한루프와 조건문을 이용해서 작성한다.

  • 실행 후 대기하다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.

public void run() {
  while(true) {   // 무한루프
    try {
      Thread.sleep(3 * 1000);   // 3초마다 반복
    } catch(InterruptedExecption e) {}
    
    // autoSave의 값이 true이면 autoSave()를 호출한다.
    if(autoSave) {
      autoSave();
    }
  }
}
  • void setDaemon(boolean on) : 쓰레드를 생성한 이후, 반드시 start()를 호출하기 전에 실행한다. 매개변수를 ture로 지정하면 데몬 쓰레드가 된다.

  • boolean isDaemon() : 쓰레드가 데몬 쓰레드인지 확인한다.

쓰레드 그룹

  • 서로 관련된 쓰레드를 그룹으로 묶어서 다룰 수 있다.

  • 모든 쓰레드는 반드시 하나의 쓰레드 그룹에 속해야 하는데, 지정하지 않으면 자동으로 ‘main쓰레드 그룹'에 속한다.

  • ThreadGroup getThreadGroup() : 쓰레드 자신이 속한 쓰레드 그룹을 반환한다.

  • void uncaughtException(Thread t, Throwable e) : 처리되지 않은 예외에 의해 쓰레드 그룹이 종료되었을 때, JVM에 의해 이 메소드가 자동으로 호출된다

  • 자신을 생성한 쓰레드(부모 쓰레드)의 그룹과 우선순위를 상속받는다.

쓰레드 그룹 생성자

생성자 설명
Thread(ThreadGroup group, Runnable target) Allocates a new Thread object.
Thread(ThreadGroup group, Runnable target, String name) Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group.
Thread(ThreadGroup group, Runnable target, String name, long stackSize) Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group, and has the specified stack size.
Thread(ThreadGroup group, String name) Allocates a new Thread object.

동기화

**쓰레드의 동기화란?**
- 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 막는 것이다.
  • 멀티 쓰레드 프로세스에서는 여러 쓰레드가 같은 자원을 공유하기 때문에 메모리도 공유한다.

  • 어떤 한 쓰레드가 작업을 못 마치고 다른 쓰레드로 넘어갔을 때, 다른 쓰레드에 영향을 미칠 수 있다.

  • 이 때, 다른 쓰레드가 간섭받기 못하게 하려면 ‘동기화'가 필요하다.

  • 임계영역 : 동기화하기위해 간섭받지 않아야 하는 문장들

  • 임계영역은 락(Lock)을 얻은 단 한 개의 쓰레드만 출입 가능하다. (객체 1개 = 락 1개)

  • 임계영역은 1번에 1개의 쓰레드만 수행되기 때문에 최소화해야 효율성이 좋다.

synchronized로 임계영역을 설정하는 방법

  • 메서드 전체를 임계 영역으로 지정하는 방법
// 출금 작업이 완료될때까지 다른 작업이 수행될 수 없음
public synchronized void withdraw(int money) {
  if(balance >= money) {
    try {
      Thread.sleep(1000);
    } catch (Exception e) {}
    balance -= money;
  }
}
  • 특정한 영역을 임계 영역으로 지정하는 방법
public void withdraw(int money) {

  // 임계 영역-------------------------
  synchronized(객체의 참조변수) {
    if(balance >= money) {
      try {
        Thread.sleep(1000);
      } catch (Exception e) {}
      balance -= money;
    }
  }
  // ---------------------------------
  
}

동기화 효율을 높이는 방법

  • wait() , notify()

  • wait() : 객체의 lock을 풀고 쓰레드를 해당 객체의 waiting pool에 넣는다.
  • notify() : waiting pool에서 대기 중인 쓰레드 중 하나를 깨운다.
  • notifyAll() : waiting pool에서 대기 중인 모든 쓰레드를 깨운다.

  • 동기화되어 있으면 lock은 객체 1개 당, 하나만 가질 수 있기 때문에 잠시 lock을 풀고 다른 쓰레드가 활용할 수 있도록 하는 것이다.

데드락