본문 바로가기
네이버클라우드/JAVA 웹 프로그래밍

JAVA 38일차 (2023-07-14) 자바 프로그래밍_스레드, 임계영역(Critical Region, Critical Section)

by prometedor 2023. 7. 14.
- 자바 프로그래밍
  - 스레드 프로그래밍(com.eomcs.concurrent.ex5 ~ ex7)
- 멀티태스킹의 메커니즘 이해
  - 임계영역(Critical Region, Critical Section): 세마포어(Semaphore)와 뮤텍스(Mutex)

 

멀티 스레딩(비동기 프로그래밍)의 문제점 - 사례 1 

// 멀티 스레딩(비동기 프로그래밍)의 문제점 - 사례 1 
package com.eomcs.concurrent.ex5;

public class Exam0110 {

  static class MyList {
    int[] values = new int[100];
    int size;

    public void add(int value) {

      if (size >= values.length) {
        delay();
        return;
      }
      delay();
      values[size] = value;
      delay();
      size = size + 1;
      delay();
    }

    public void print() {
      for (int i = 0; i < size; i++) {
        System.out.printf("%d:  %d\n", i, values[i]);
      }
    }

    public void delay() {
      int count = (int)(Math.random() * 1000);
      for (int i = 0; i < count; i++) {
        Math.atan(34.1234);
      }
    }
  }

  static class Worker extends Thread {
    MyList list;
    int value;

    public Worker(MyList list, int value) {
      this.list = list;
      this.value =  value;
    }

    @Override
    public void run() {
      for (int i = 0; i < 20; i++) {
        list.add(value);
      }
    }
  }

  public static void main(String[] args) throws Exception {
    MyList list = new MyList();

    Worker w1 = new Worker(list, 111);
    Worker w2 = new Worker(list, 222);
    Worker w3 = new Worker(list, 333);

    w1.start();
    w2.start();
    w3.start();

    Thread.sleep(10000);

    list.print();
  }

}

=>

ㄴ 스레드가 동시에 이 메서드에 진입하면 배열의 값을 덮어쓰는 문제가 발생
ㄴ 이렇게 여러 스레드가 동시에 접근했을 때 문제가 발생하는 코드 부분을 "Critical Section" 또는 "Critical Region" 이라 부름

 

 

멀티 스레딩(비동기 프로그래밍)의 문제점 - 사례 1의 문제 해결

// 멀티 스레딩(비동기 프로그래밍)의 문제점 - 사례 1의 문제 해결
package com.eomcs.concurrent.ex5;

public class Exam0120 {

  static class MyList {
    int[] values = new int[100];
    int size;

    // 다음 메서드를 동기화 처리해 보자.
    // => synchronized
    //    - 크리티컬 섹션 구간에 이 키워드를 붙이면 오직 한 번에 한 개의 스레드만이 접근할 수 있다.
    synchronized public void add(int value) {
      if (size >= values.length) {
        delay();
        return;
      }
      delay();
      values[size] = value;
      delay();
      size = size + 1;
      delay();
    }

    public void print() {
      for (int i = 0; i < size; i++) {
        System.out.printf("%d:  %d\n", i, values[i]);
      }
    }

    public void delay() {
      int count = (int)(Math.random() * 1000);
      for (int i = 0; i < count; i++) {
        Math.atan(34.1234);
      }
    }
  }

  static class Worker extends Thread {
    MyList list;
    int value;

    public Worker(MyList list, int value) {
      this.list = list;
      this.value =  value;
    }

    @Override
    public void run() {
      for (int i = 0; i < 20; i++) {
        list.add(value);

        // add() 호출 후 다른 스레드에게 CPU 사용권을 뺏길 기회를 만들자!
        int count = (int)(Math.random() * 1000);
        for (int x = 0; x < count; x++) {
          Math.atan(34.1234);
        }
      }
    }
  }

  public static void main(String[] args) throws Exception {
    MyList list = new MyList();

    Worker w1 = new Worker(list, 111);
    Worker w2 = new Worker(list, 222);
    Worker w3 = new Worker(list, 333);

    w1.start();
    w2.start();
    w3.start();

    Thread.sleep(10000);

    list.print();
  }

}

ㄴ  Critical Section에 오직 한 개의 스레드만 접근하게 하면 비동기로 인한 문제가 발생하지 않음
     => 동기화로 처리함
     => 동기화? 
        ㄴ 여러 스레드가 동시에 실행하는 것이 아니고 여러 스레드가 순차적으로 접근하는 것
        ㄴ 순차적으로 실행한다는 것은 동시 실행의 이점을 버리는 것이기 때문에 스레드를 사용하기 전의 상태와 같음

          => 기존의 실행 방식 처럼 실행 시간이 많이 걸림

ㄴ synchronized 를 이용해 동기화 처리

    ㄴ 크리티컬 섹션 구간에 이 키워드를 붙이면 오직 한 번에 한 개의 스레드만이 접근할 수 있
        => 먼저 접근한 스레드가 나가야만 다음 스레드가 진입할 수 있음
        => 크리티컬 섹션을 뮤텍스 구간으로 설정

 

멀티 스레딩(비동기 프로그래밍)의 문제점 - 사례 2

// 멀티 스레딩(비동기 프로그래밍)의 문제점 - 사례 2
package com.eomcs.concurrent.ex5;

public class Exam0210 {

  static class Account {
    String accountId;
    long balance;

    public Account(String accountId, long balance) {
      this.accountId = accountId;
      this.balance = balance;
    }

    public long withdraw(long money) {
    // 크리티컬 섹션

      long b = this.balance;

      delay();  // CPU를 뺏길 기회를 제공

      b -= money;

      delay();  // CPU를 뺏길 기회를 제공

      if (b < 0)
        return 0;

      delay();  // CPU를 뺏길 기회를 제공

      this.balance = b;

      delay();  // CPU를 뺏길 기회를 제공

      return money;
    }

    private void delay() {
      int delayCount = (int)(Math.random() * 1000);
      for (int i = 0; i < delayCount; i++)
        Math.asin(45.765); // CPU를 뺏길 기회를 제공
    }
  }

  static class ATM extends Thread {
    Account account;

    public ATM(String name, Account account) {
      super(name);
      this.account = account;
    }

    @Override
    public void run() {
      long money = 0;
      long sum = 0;

      while (true) {
        money = account.withdraw(100);
        if (money <= 0)
          break;
        sum += money;
      }
      System.out.printf("%s에서 찾은 돈: %d원\n", this.getName(), sum);
    }
  }

  public static void main(String[] args) {
    Account account = new Account("111-11-1111-111", 100_0000);

    ATM 강남 = new ATM("강남", account);
    ATM 서초 = new ATM("서초", account);
    ATM 부산 = new ATM("부산", account);
    ATM 대전 = new ATM("대전", account);
    ATM 광주 = new ATM("광주", account);

    강남.start();
    서초.start();
    부산.start();
    대전.start();
    광주.start();

  }
}

=>

ㄴ 여러 스레드가 같은 메모리(balance 필드)의 값을 동시에 변경할 때 문제가 발생할 수 있는 코드를 "크리티컬 섹션(임계영역; critical section)" 또는 "크리티컬 리전(critical region)" 이라 부름

ㄴ  강남, 서초 등 여러 개의 스레드가 같은 객체에 대해 메서드를 호출하여 동시에 값을 변경하려 할 때 서로 그 메모리의 값을 덮어쓰는 문제가 발생함

     => 여러 스레드가 동시에 실행할 때 문제를 일으키는 코드를 "임계 구역(Critical Section; Critical Region)"이라 부름    

          ㄴ 위 코드에서는 여러 스레드가 동시에 호출하고, 같은 인스턴스의 변수 값을 변경하는 메서드인 "withdraw()"가 critical section임

 임계 구역(critical section)
 - 여러 스레드가 동시에 실행할 때 문제가 발생하는 코드 블록을 말한다.
 - critical region 이라고도 부른다.
 - 같은 메모리에 여러 스레드가 동시에 접근하여 값을 변경하려 할 때 문제가 발생하는 것이다.
   즉 다른 스레드가 사용하는 변수의 값을 임의로 변경하면 그 스레드는 원래의 의도대로 동작하지 않을 것이다.
 - "스레드 안전(thread safe)하지 않다"라고 말한다.

 

멀티 스레딩(비동기 프로그래밍)의 문제점 - 사례 2의 문제점 해결

// 멀티 스레딩(비동기 프로그래밍)의 문제점 - 사례 2의 문제점 해결
package com.eomcs.concurrent.ex5;

public class Exam0220 {

  static class Account {
    String accountId;
    long balance;

    public Account(String accountId, long balance) {
      this.accountId = accountId;
      this.balance = balance;
    }

    // 한 번에 한 스레드 만이 호출하도록 접근을 제한하고 싶다면
    // 메서드 전체를 동기화 블록으로 선언하라!
    //
    synchronized public long withdraw(long money) {
      long b = this.balance;

      delay();  // CPU를 뺏길 기회를 제공

      b -= money;

      delay();  // CPU를 뺏길 기회를 제공

      if (b < 0)
        return 0;

      delay();  // CPU를 뺏길 기회를 제공

      this.balance = b;

      delay();  // CPU를 뺏길 기회를 제공

      return money;
    }

    private void delay() {
      int delayCount = (int)(Math.random() * 1000);
      for (int i = 0; i < delayCount; i++)
        Math.asin(45.765); // CPU를 뺏길 기회를 제공
    }
  }

  static class ATM extends Thread {
    Account account;

    public ATM(String name, Account account) {
      super(name);
      this.account = account;
    }

    @Override
    public void run() {
      long money = 0;
      long sum = 0;

      while (true) {
        money = account.withdraw(100);
        if (money <= 0)
          break;
        sum += money;
      }
      System.out.printf("%s에서 찾은 돈: %d원\n", this.getName(), sum);
    }
  }

  public static void main(String[] args) {
    Account account = new Account("111-11-1111-111", 100_0000);

    ATM 강남 = new ATM("강남", account);
    ATM 서초 = new ATM("서초", account);
    ATM 부산 = new ATM("부산", account);
    ATM 대전 = new ATM("대전", account);
    ATM 광주 = new ATM("광주", account);

    강남.start();
    서초.start();
    부산.start();
    대전.start();
    광주.start();

    // 비동기 문제 해결책?
    // => 한 번에 한 스레드 만이 크리티컬 섹션을 실행하도록 접근을 제한하면 된다.
  }
}

=>

ㄴ  한 번에 한 스레드 만이 호출하도록 접근을 제한하고 싶다면 메서드 전체를 동기화 블록으로 선언하라!
     => 메서드 앞에 synchronized를 붙이는 방법을 이용

ㄴ 한 번에 한 스레드 만이 크리티컬 섹션을 실행하도록 접근을 제한하혀 비동기 문제 해결 가능

크리티컬 섹션에 동시에 접근하지 못하게 하는 기법
     => "뮤텍스(mutex)" 또는 "세마포어(1)(semaphore)"라 부른다.
     
자바에서 뮤텍스를 구현하는 방법,
     => 크리티컬 섹션에 해당하는 메서드나 코드 블록에 sychronized 키워드를 붙여
        한 번에 한 스레드만 진입할 수 있도록 lock을 건다.
     
     참고!
     => 여러 스레드가 동시에 실행해도 문제가 없는 코드 블록을
        "스레드 안전(thread safe)"라 부른다.
    

비동기 문제 해결책?
     => 한 번에 한 스레드 만이 크리티컬 섹션을 실행하도록 접근을 제한하면 된다.
     
     주의!
     => 동시에 여러 스레드가 같은 메모리에 접근하더라도 값을 변경하는 것이 아니라 단순히 값을 조회하는 경우에는 
        멀티 스레드 문제가 발생하지 않는다.

 

synchronized - 메서드를 동기화시킴

// synchronized - 메서드를 동기화시킴
package com.eomcs.concurrent.ex5;

public class Exam0310 {
  static class Counter {
    long value;

    public Counter(long value) {
      this.value = value;
    }
  }

  // 비동기 실행
  // - 여러 스레드가 동시에 진입 가능!
  static void print1(String threadName, Counter counter) {
    System.out.printf("[%s] 출력 시작 ----------\n", threadName);

    for (int i= 0; i < counter.value; i++) {
      System.out.printf("%s ==> %d\n", threadName, i);
    }

    System.out.printf("---------- [%s] 출력 끝\n", threadName);
  }

  // 동기 실행
  // - 한 번에 한 스레드만 진입 가능!
  synchronized static void print2(String threadName, Counter counter) {
    System.out.printf("[%s] 출력 시작 ----------\n", threadName);

    for (int i  = 0; i < counter.value; i++) {
      System.out.printf("%s ==> %d\n", threadName, i);
    }

    System.out.printf("---------- [%s] 출력 끝\n", threadName);
  }

  // 동기화 블록
  // - 한 번에 한 스레드만 진입 가능!
  static void print3(String threadName, Counter counter) {
    System.out.printf("[%s] 출력 시작 ----------\n", threadName);

    synchronized (counter) {
      System.out.printf("[%s] $$$$$$$$$$$$$$$$$$$$$$\n", threadName);
      for (int i  = 0; i < 1000; i++) {
        System.out.printf("%s ==> %d\n", threadName, i);
      }
    }

    System.out.printf("---------- [%s] 출력 끝\n", threadName);
  }

  static class Worker extends Thread {
    Counter counter;

    public Worker(String name, Counter counter) {
      super(name);
      this.counter = counter;
    }

    @Override
    public void run() {
      print3(this.getName(), counter);
    }
  }

  public static void main(String[] args) throws Exception {
    Counter counter = new Counter(1000);

    Worker w1 = new Worker("**홍길동", counter);
    Worker w2 = new Worker("임꺽정---->", counter);
    Worker w3 = new Worker("유%관%순", counter);

    w1.start();
    w2.start();
    w3.start();
  }
}

=>

...

...

...

...

 

----------------------------------------------------------------------

 

스레드 재사용 - 1단계) 스레드 재 사용전 - 매번 스레드 생성

// 스레드 재사용 - 1단계) 스레드 재 사용전 - 매번 스레드 생성
package com.eomcs.concurrent.ex6;

import java.util.Scanner;

public class Exam0110 {

  public static void main(String[] args) {

    class MyThread extends Thread {
      int count;

      public void setCount(int count) {
        this.count = count;
      }

      @Override
      public void run() {
        for (int i = count; i > 0; i--) {
          System.out.println("==> " + i);
        }
      }
    }

    Scanner keyScan = new Scanner(System.in);

    while (true) {
      System.out.print("카운트? ");
      String str = keyScan.nextLine();
      if (str.equals("quit")) {
        break;
      }

      int count = Integer.parseInt(str);

      MyThread t = new MyThread();
      t.setCount(count);
      t.start();
      // 카운트 할 때 마다 매번 스레드를 생성한다.
      // => 실행 완료된 스레드는 가비지가 된다.
      // => 즉 스레드를 매번 생성하는 방식은 과다한 가비지를 생성하기 때문에 메모리 낭비를 일으킨다.
      // => 스레드는 실제 OS가 생성한다.
      //    즉 스레드를 생성하는데 시간이 소요된다는 것이다.
      //    스레드를 자주 생성하면 결국 스레드 생성하는데 시간을 많이 소요하게 된다.
    }

    System.out.println("main 스레드 종료!");
    keyScan.close();
  }
}

=>

ㄴ 카운트 할 때 마다 매번 스레드를 생성하면 실행 완료된 스레드는 가비지가 되어 메모리 낭비가 발생함

 

스레드 재사용 - 1단계) 스레드를 재 사용하려 시도

// 스레드 재사용 - 1단계) 스레드를 재 사용하려 시도
package com.eomcs.concurrent.ex6;

import java.util.Scanner;

public class Exam0111 {

  public static void main(String[] args) {

    class MyThread extends Thread {
      int count;

      public void setCount(int count) {
        this.count = count;
      }

      @Override
      public void run() {
        for (int i = count; i > 0; i--) {
          System.out.println("==> " + i);
        }
      }
    }

    // 카운트를 수행할 스레드를 미리 만든다.
    MyThread t = new MyThread();

    Scanner keyScan = new Scanner(System.in);

    while (true) {
      System.out.print("카운트? ");
      String str = keyScan.nextLine();
      if (str.equals("quit")) {
        break;
      }

      // 사용자가 카운트 값을 입력하면, 
      int count = Integer.parseInt(str);

      // 기존에 생성한 스레드에 카운트 값을 설정한 후 실행을 시작시킨다.
      t.setCount(count);
      t.start();
      // 문제점?
      // - 한 번 실행이 완료된 Dead 상태의 스레드는 다시 시작시킬 수 없다.
    }

    System.out.println("main 스레드 종료!");
    keyScan.close();
  }
}

=>

ㄴ 위 코드에서 카운트를 수행할 스레드는 두 번째 호출 될 때 기존 스레드를 호출함

ㄴ 한 번 실행이 완료된 Dead 상태의 스레드는 다시 시작시킬 수 없음

 

스레드 재사용 - 2단계) sleep()/timeout 을 활용한 스레드 재사용

// 스레드 재사용 - 2단계) sleep()/timeout 을 활용한 스레드 재사용
package com.eomcs.concurrent.ex6;

import java.util.Scanner;

public class Exam0120 {

  public static void main(String[] args) {

    class MyThread extends Thread {
      int count;

      public void setCount(int count) {
        this.count = count;
      }

      @Override
      public void run() {
        System.out.println("스레드 시작했음!");
        try {
          // 스레드를 재사용하려면 다음과 같이 run() 메서드가 종료되지 않게 해야 한다.
          while (true) {
            // 사용자가 카운트 값을 입력할 시간을 주기 위해 10초 정도 스레드를 멈춘다.
            Thread.sleep(10000);

            System.out.println("카운트 시작!");
            for (int i = count; i > 0; i--) {
              System.out.println("==> " + i);
            }
          }
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }

    MyThread t = new MyThread();

    // 미리 스레드를 시작시켜 놓는다.
    t.start();

    Scanner keyScan = new Scanner(System.in);

    while (true) {
      System.out.print("카운트? ");
      String str = keyScan.nextLine();

      if (str.equals("quit")) {
        break;
      }

      int count = Integer.parseInt(str);
      t.setCount(count); // 스레드의 카운트 값을 변경한다.
    }

    System.out.println("main 스레드 종료!");
    keyScan.close();
  }
}

ㄴ sleep()을 이용한 스레드 재활용 방식은 일정 시간이 지난 후 스레드가 작업하게 만드는 방식임
ㄴ 스레드가 잠든 사이에 작업할 내용을 설정해두면, 스레드가 깨어났을 때 변경 사항에 따라 작업을 수행함
     =>이 방식으로 한 개의 스레드를 재활용하여 작업을 처리할 수 있지만, 
    문제는:
     => 스레드가 깨어날 때까지 작업이 바로 실행되지 않음
     => 작업을 시키고 싶지 않아도 깨어나면 무조건 작업할 것임

=>

ㄴ 값을 다시 세팅하지 않아도 10초마다 똑같은 값이 반복돼서 출력됨

 

스레드 재사용 - 3단계) sleep()/timeout 을 활용한 스레드 재사용 II

// 스레드 재사용 - 3단계) sleep()/timeout 을 활용한 스레드 재사용 II
package com.eomcs.concurrent.ex6;

import java.util.Scanner;

public class Exam0130 {

  public static void main(String[] args) {

    class MyThread extends Thread {
      boolean enable;
      int count;

      public void setCount(int count) {
        this.count = count;

        // 카운트 값을 설정할 때 작업을 활성화시킨다.
        this.enable = true;
      }

      @Override
      public void run() {
        System.out.println("스레드 시작했음!");
        try {
          while (true) {
            System.out.println("스레드를 10초 동안 잠들게 한다!");
            Thread.sleep(10000);

            // 무조건 작업하지 말고,
            // enable이 true일 때만 작업하게 하자!
            if (!enable) {
              continue;
            }

            System.out.println("카운트 시작!");
            for (int i = count; i > 0; i--) {
              System.out.println("==> " + i);
            }
            // 스레드에게 맡겨진 작업이 끝나면 비활성 상태로 설정한다.
            enable = false;
          }
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }

    MyThread t = new MyThread();
    t.start();

    Scanner keyScan = new Scanner(System.in);

    while (true) {
      System.out.print("카운트? ");
      String str = keyScan.nextLine();
      if (str.equals("quit")) {
        break;
      }

      int count = Integer.parseInt(str);
      t.setCount(count);
      
    }

    System.out.println("main 스레드 종료!");
    keyScan.close();
  }
}

ㄴ 스레드가 작업을 완료하면 10초 동안 잠듦
ㄴ 10초 후에 깨어났을 때 카운트 값이 설정되어 있지 않으면 다시 잠듦
ㄴ 카운트 값이 설정되면서 enable이 활성화 상태라면 작업을 실행함
ㄴ 작업이 끝나면 enable을 비활성으로 만든 후 잠듦
     => 이전 버전에서는 깨어난 후 무조건 작업을 수행했지만,
     => 이 버전은 카운트 값이 설정될 때만 작업하도록 개선한 것
           그러나 근본적인 문제는 해결되지 않음
           => 작업을 완료한 후 무조건 10초를 기다림
           => 스레드가 깨어난 후 작업이 없더라도 10초를 기다림

=>

ㄴ 실행 한 후 값을 주지 않으면 스레드를 10초 동안 잠들게 하고 값을 입력하면 카운트 값 출력

ㄴ 단점은 값을 줘도 10초를 기다려야함

 

 

스레드 재사용 - 4단계) wait()/notify() 사용

// 스레드 재사용 - 4단계) wait()/notify() 사용
package com.eomcs.concurrent.ex6;

import java.util.Scanner;

public class Exam0140 {

  public static void main(String[] args) {

    class ValueBox {
      int count;

      synchronized  public void setCount(int count) {
        this.count = count;

        // 이 객체의 사용을 기다리는 스레드에게 작업을 시작할 것을 알린다.
        //synchronized (this) {
        this.notify();
        //}
        // 문법 주의!
        // => notify()도 동기화 영역에서 호출해야 한다.
        // => 안그러면 IllegalMonitorStateException 예외가 발생한다.
      }
    }

    class MyThread extends Thread {
      ValueBox valueBox;

      public void setValueBox(ValueBox valueBox) {
        this.valueBox = valueBox;
      }

      @Override
      public void run() {

        System.out.println("스레드 시작했음!");
        try {
          while (true) {
            System.out.println("스레드 대기중...");

            // wait()
            // - 해당 객체에서 notify()를 통해 알림이 올 때까지 스레드의 실행을 멈추게 한다.
            // - 이 메서드는 동기화 블록
            //   (한 번에 한 스레드만이 진입하도록 설정된 블록)에서만 호출할 수 있다.
            
            synchronized (valueBox) {
              valueBox.wait();
              // valueBox 객체에 대해 사용하라는 신호가 올 때까지 이 스레드에게 기다리라는 명령이다.
              // 즉 wait()를 호출한 스레드는 Not Runnable 상태에 진입한다.
              // => 실행을 멈추고 CPU 사용권을 받지 않는 상태가 된다.
            }
            System.out.println("카운트 시작!");
            for (int i = valueBox.count; i > 0; i--) {
              System.out.println("==> " + i);
            }
          }
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }

    ValueBox valueBox = new ValueBox();

    MyThread t = new MyThread();
    t.setValueBox(valueBox);
    t.start(); // 이 스레드는 main 스레드가 실행하라고 신호를 줄 때까지 기다린다

    Scanner keyScan = new Scanner(System.in);

    while (true) {
      System.out.print("카운트? ");
      String str = keyScan.nextLine();
      if (str.equals("quit")) {
        break;
      }
      valueBox.setCount(Integer.parseInt(str));
      // setCount()
      // - 사용자가 입력한 카운트 값을 설정할 때
      // - main 스레드는 이 객체의 사용을 간절히 기다리는 다른 스레드에게 
      //   즉시 사용하라고 신호를 보낸다.
    }

    System.out.println("main 스레드 종료!");
    keyScan.close();
  }
}

ㄴ 문법 주의!
    => wait()/notify() 는 반드시 동기화 영역 안에서 호출해야 함
          ㄴ 동기화 영역?
               => synchronized로 선언된 메서드
                   예) synchronized void m() {}
              => synchronized로 묶인 블록
                   예) synchronized(접근대상) {...}

ㄴ valueBox.wait();
    ㄴ valueBox 객체에 대해 사용하라는 신호가 올 때까지 이 스레드에게 기다리라는 명령
        => 즉 wait()를 호출한 스레드는 Not Runnable 상태에 진입함
     => 실행을 멈추고 CPU 사용권을 받지 않는 상태가 됨
    ㄴ 이 스레드를 잠에서 깨우는 방법?
        => 다른 스레드가 valueBox에 대해 notify()를 호출하면, wait() 로 신호를 기다리고 있는 스레드가 잠에서 깨어나 실행을 시작함
    ㄴ 기다림을 끝내는 방법?
     => notify()를 통해 기다림이 끝났다는 것을 알림 받아야 함

=>

=>

ㄴ 값을 입력하면 바로 출력되고, 입력하지 않으면 바로 스레드 대기중이되고, 다시 값을 입력하면 notify 가 호출되어 깨워주고 값을 출력함

ㄴ 스레드는 종료되지 않음

 

멀티 스레드 재사용 - Pooling 기법을 이용하여 생성된 객체를 재활용하기

// 멀티 스레드 재사용 - Pooling 기법을 이용하여 생성된 객체를 재활용하기
package com.eomcs.concurrent.ex6;

import java.util.ArrayList;
import java.util.Scanner;

public class Exam0210 {

  static class MyThread extends Thread {
    ThreadPool pool;
    int count;

    public MyThread(String name, ThreadPool pool) {
      super(name);
      this.pool = pool;
    }

    public void setCount(int count) {
      this.count = count;
      synchronized (this) {
        notify();
      }
    }

    @Override
    public void run() {
      synchronized (this) {
        try {
          while (true) {
            // 작업하라는 알림이 올 때까지 기다린다.
            wait();

            // 알림이 오면 작업을 실행한다.
            for (int i = count; i > 0; i--) {
              System.out.printf("[%s] %d\n", getName(), i);
              Thread.sleep(2000);
            }

            // 작업이 끝났으면 스레드풀로 돌아간다.
            pool.add(this);
          }
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }
  }

  // MyThreadPool 과 MyThread 상호간에 참조를 피하기 위해
  // 인터페이스를 준비했다.
  interface ThreadPool {
    Thread get();
    void add(Thread obj);
  }

  static class MyThreadPool implements ThreadPool {
    ArrayList<MyThread> list = new ArrayList<>();

    public MyThreadPool() {
      // 사용할 스레드 객체를 미리 생성한다.
      // - 나중에 MyThread가 Pool로 다시 리턴될 수 있도록
      //   스레드 객체를 생성할 때 Pool의 주소를 알려준다.
      MyThread t1 = new MyThread("1번 스레드=>", this);
      MyThread t2 = new MyThread("2번 스레드***>", this);
      MyThread t3 = new MyThread("3번 스레드-->", this);

      // 생성된 스레드를 컬렉션에 보관한다.
      list.add(t1);
      list.add(t2);
      list.add(t3);

      // 일단 무조건 스레드를 미리 실행해 놓는다.
      t1.start();
      t2.start();
      t3.start();
    }

    // 스레드 풀에서 한 개의 스레드를 꺼낸다.
    @Override
    public MyThread get() {
      if (list.size() > 0) { // 컬렉션에 남아 있는 스레드가 있다면,
        return list.remove(0);
      }
      return null; // 없으면, null을 리턴한다.
      // 현재 이 예제에서는 오직 3개의 스레드만 쓰도록 하였다.
    }

    // 스레드를 다 쓴 후에는 다시 스레드 풀에 돌려준다.
    @Override
    public void add(Thread t) {
      list.add((MyThread) t);
    }
  }

  public static void main(String[] args) {

    // 스레드풀 준비!
    MyThreadPool threadPool = new MyThreadPool();

    Scanner keyScan = new Scanner(System.in);

    while (true) {
      System.out.print("카운트? ");
      String str = keyScan.nextLine();
      if (str.equals("quit")) {
        break;
      }

      int count = Integer.parseInt(str);

      // 스레드풀에서 스레드를 한 개 꺼낸다.
      MyThread t = threadPool.get();
      if (t == null) {
        System.out.println("남는 스레드가 없습니다!");
        continue;
      }

      // 스레드의 카운트를 설정한다. 그러면 카운트를 시작할 것이다.
      t.setCount(count);
    }

    System.out.println("main 스레드 종료!");
    keyScan.close();
  }
}

ㄴ 3개의 스레드만 쓰도록 함

ㄴ 스레드를 다 쓴 후에는 다시 스레드 풀에 돌려줌

=>

ㄴ 3개의 스레드를 돌려 씀