Multi-thread : producer - consumer

Post on 20-Aug-2015

1.023 views 3 download

Transcript of Multi-thread : producer - consumer

Multi-ThreadProducer-Consumer pattern

Thread 를 알아야 하는 이유 ?

한발 더 나아가 미래를 내다보는 개발자는 이미 단일 컴퓨터 내부에서의 병렬 처리를 넘어 여러 대의 컴퓨터에서 병렬로 동작하는 플랫폼을 찾고 있고 , 이와 같은 분산 병렬 처리 플랫폼이 한창 인기를 얻는 요즘입니다 . 이런 대규모 병렬 처리 플랫홈도 중요하긴 하지만 , 그 안에서는 항상 단일 프로세스 내부에서 동작하는 여러 스레드가 안정적으로 실행되도록 하는 병렬 처리 기법이 적용되어 있음을 잊어서는 안될 것입니다 .

-’ 멀티 코어를 100% 활용하는 자바 병렬 프로그래밍’ 옮김이의 말 중

Thread 를 판단하는 기준 ??

1. Safety2. Liveness3. Performance4. Reuse

1. Safety

Thread 에서 사용되는 값들의 무결성

정확성 !! Class 가 해당 Class 명세에 부합한다 .

해당 객체가 어떻게 생성되고 어떻게 사용되든 다른 작업 없이 ,정확하게 동작하면 해당 객체는 Thread Safe 하다고 말한다 .

2. Liveness

원하는 일이 원하는 시기에 동작한다 .

Deadlock 서로 다른 Thread 가 서로 가지고 있는 독점 자원을 원하여 멈춰져 있는 현상

Starvation Thread 가 자신이 원하는 자원을 할당받지 못하는 상황

Livelock 서로 다른 Thread 가 서로의 상태를 변경하면서 작업을 진행하지 못하는 현상

3. Performance

Muti-Thread 환경은 Single-Thread 환경보다 성능상 자원을 더 소모한다 .

( 락 , 메모리 , 상태 설정 , Thread 생성 및 제거 등등 )

Muti-thread 는 결국 !!

이런 비용을 지불하여 응답성과 처리용량을 늘리는 것이 목적 !!!

서로 다른 Thread 의 경쟁할 일이 없는 synchonized 블록의 경우 JVM 수준에서 동기화 관련 부하를 줄이거나 아예 없애주기 때문에 매우 빠르게 동작한다 . 실행 순서를 고정하는 역할을 할 수도 있다 .

대신 경쟁조건이 존재하는 경우 대부분의 JVM 이 운영체제의 기능을 호출한다 .

4. Reuse

Synchonized 키워드 , volatile 키워드• 성능을 위해서는 위에 두 키워드 뿐 아니라 Multi-thread 의 기법에

들어가는 모든 기술을 최소화하는 것이 좋다 .

하지만 !! 이런 키워드가 없는 경우• 후임 개발자가 해당 Class 에 대해 Thread safe 여부를 판단하기 힘들고

더 심한 경우에는 이미 안전한 Class 를 망칠 수도 있다 .

Synchonized 키워드를 사용하지 않아도 되는 Class

public final class Person { private final String name; public Person(String name){ this.name = name; } public String getName(){ return name; } public String toString(){ return "Person :: name = "+name; }}

public class SyncPerson { private String name; public SyncPerson(String name){ this.name = name; } public synchronized String getName(){ return name; } public synchronized void setName(String name){ this.name = name; } public synchronized String toString(){ return "Person :: name = " + name; }}

for(long i=0;i<CALL_COUNT;i++){ Person person = new Person(String.valueOf(i)); person.toString(); }

SyncPerson syncPerson = new SyncPerson(String.valueOf(0)); for(long i=0;i<CALL_COUNT;i++){ syncPerson.setName(String.valueOf(i)); syncPerson.toString(); }

Person : BEGINPerson : ENDtime = 475msec.

SyncPerson : BEGINSyncPerson : ENDtime = 523msec.

public class TestMain { private static final long CALL_COUNT = 10000000L; public static void main(String args []){ new PersonConsumerThread("Single Thread",CALL_COUNT).run();

Thread t1 = new PersonConsumerThread("Thread-0", CALL_COUNT/2); t1.start(); Thread t2 = new PersonConsumerThread("Thread-1", CALL_COUNT/2); t2.start(); //대기 try { t1.join(); t2.join(); } catch(InterruptedException e) { }

SyncPerson syncPerson = new SyncPerson(String.valueOf(0)); new SyncPersonCounmerThread("Single Thread", CALL_COUNT, syncPerson).run();

Thread t4 = new SyncPersonCounmerThread("Thread-4", CALL_COUNT/2, syncPerson); t4.start(); Thread t5 = new SyncPersonCounmerThread("Thread-5", CALL_COUNT/2, syncPerson); t5.start(); }}

Single Thread Person : BEGIN 10000000Single Thread Person : END time = 534msec.Main Person : BEGINThread-0 Person : BEGIN 5000000Thread-1 Person : BEGIN 5000000Thread-0 Person : END time = 297msec.Thread-1 Person : END time = 298msec.Main Person : END time = 298msec.

Single Thread SyncPerson : BEGIN 10000000Single Thread SyncPerson : END time = 668msec.Main SyncPerson : BEGINThread-4 SyncPerson : BEGIN 5000000Thread-5 SyncPerson : BEGIN 5000000Thread-4 SyncPerson : END time = 1110msec.Thread-5 SyncPerson : END time = 1121msec.Main SyncPerson : END time = 1790msec.

Immutable??? public static void main(String args[]){ Person person = new Person(“bob”); …… System.out.println(person.getName()); System.out.println(person.getName().replace(“b”,”p”)); System.out.println(person.getName());}bobpopbob

String 의 replace() 를 사용하면 실제 객체 값에는 영향을 주지 않고 그 순간만 데이터를 바꿔서 사용할 수 있다 .

String 에 replace() 같은 메소드가 문제 !

다행히 String 에서는 별 문제 없지만…

public final class Person { private final StringBuffer info; public Person(String name){ this.info = new StringBuffer("name = “ + name); } public StringBuffer getInfo(){ return info; } public String toString(){ return "Person :: " + info; }}

public static void main(String args[]){ Person person = new Person("bob"); System.out.println(person); StringBuffer name = person.getInfo(); name.replace(7,10,"pop"); System.out.println(person);}

Person :: name = bobPerson :: name = pop

StringBuffer 에서 제공하는 replace() 는 String 과 달 리 객 체 에 직 접 영 향 을 주 기 때 문 에 Muti-thread 환경에서 안전하지 못하다 .

Thread 종료

Thread 의 종료는 매우 중요하다 !!• Thread 가 정상적으로 종료하지 않으면 , -> JVM 역시 종료되지 않고 대기하게 된다 .

실행 중인 Thread 를 그냥 종료 ???• 작업 중이던 메모리에 대한 반환 작업이 정상적으로 이뤄지지 않는 경우도

있고 해서 Thread.stop() 과 Thread.suspend() 와 같은 메소드는 사용하면 안된다 !!!(deprecated)

JAVA 에서는 특정 Thread 를 종료시키면 안된다 !!• 작업을 수행하는 Thread 와 해당 Thread 의 종료를 요청하는 Thread

를 같이 생각해야 한다 .

public class PrimeGenerator implements Runnable{ private final List<BigInteger> primes = new ArrayList<BigInteger>(); private volatile Boolean cancelled;  public void run(){ BigInteger p = BigInteger.ONE; while(!cancelled) { p = p.nextProbablePrime(); synchronized(this){ primes.add(p); } } } public void cancel(){ cancelled = true; }}

해당 cancel 메소드를 호출하면 해당 Thread 는 작업을 정리한 후에 종료한다 .

해당 Thread 가 sleep 이나 join 같은 대기 상태라면 ???

public class PrimeGenerator implements Runnable{ private final List<BigInteger> primes = new ArrayList<BigInteger>(); private volatile Boolean cancelled;  public void run(){ BigInteger p = BigInteger.ONE; Thread.sleep(1000000); while(!cancelled) { p = p.nextProbablePrime(); synchronized(this){ primes.add(p); } } } public void cancel(){ cancelled = true; }}

해당 Thread 는 대기기간이 끝날 때까지Cancelled 가 아무런 영향도 미치지 못한다 .

Interrupted 를 활용하자 !!public class PrimeGenerator implements Runnable{ private final List<BigInteger> primes = new ArrayList<BigInteger>();  public void run(){ BigInteger p = BigInteger.ONE; while(!Thread.currentThread().isInterrupted()) { p = p.nextProbablePrime(); synchronized(this){ primes.add(p); } } }}

public class PrimeGenerator implements Runnable{ private final List<BigInteger> primes = new ArrayList<BigInteger>();  public void run(){ BigInteger p = BigInteger.ONE; while(!Thread.currentThread().isInterrupted()) { try { Thread.sleep(5000); } catch (InterruptedException e) { } p = p.nextProbablePrime(); synchronized(this){ primes.add(p); } } }}

다른 대기 중이라도 interrupt 가 발생하면 대기 상태에서 벗어나게 된다 .종종 대기 상태인 thread 를 깨우기 위해 interrupt 를 사용하는 경우 두 interrupt 의 용도가 혼동될 수 있다 . Interrupt 를 두 번 연속으로 주면 될 것 같지만 소용없다 . 이런 때는 앞의 cancel 메소드를 사용하자 .

notify vs notifyAll

초기상태

실행

실행 가능

실행 상태

종료

sleepTIME_WATIN

GjoinTIME_WATIN

Gwait

TIME_WATING

BLOCKED

Thread 상태 변화 다이어그램Thread 생성

Start 메소드

schedule

Run 메소드 종료

GC

sleep

join

wait

Synchronized

I/O 대기 등 ..

interruptjoin 종료time out

interruptnotify/notifyAlltime out

interruptsleep 종료time out

Lock 획득

notify 는 wait set 에서 대기하는 thread 중 하나만 실행하고notifyAll 은 wait set 에 들어가 있는 모든 thread 를 실행한다 .경쟁에서 밀린 thread 는 다시 wait set 으로 들어가게 된다 .

그렇지만 notify 가 더 좋다고 할 수는 없다 .우선 notify 가 깨우는 thread 는 JVM 과 운영체제가 결정하고어떤 thread 가 동작하는지 알 수가 없다 . 그리고…

public class LazyThread extends Thread{ public void run() { while (true) { try { synchronized (table) { table.wait(); } System.out.println(getName() + " is notified!"); } catch (InterruptedException e) { } } }} notify 로 해당 thread 를 깨우려고 해도 내부의 table.wait() 만 깨우고

정작 thread 는 동작하지 않는다 .

Exception

throws ?? try-catch??public class FinallyThread extends Thread { public void run(){ try{ Thread.sleep(1000000); doWork(); }catch (InterruptedException e){ }finally { doShutdown(); } } private void doWork() throws InterruptedException { //동작 Thread.sleep(500); } private void doShutdown(){ //종료작업 }}

자원을 Thread 별로 잡고 있는 경우라면 반드시 try-catch –finally 로 close 를 시켜줘야 한다 .

Thread 가 동작할 준비가 되었는가 ??

지나가기

import java.util.Queue;import java.util.LinkedList; public class RequestQueue { private final Queue<Request> queue = new LinkedList<Request>(); public synchronized Request getRequest() { if(queue.peek() == null) { return; } return queue.remove(); } public synchronized void putRequest(Request request) { queue.offer(request); }}

return 에 boolean 값이나 null 값으로 처리형태를 알려줄 수 있다 .혹은 throw exception 으로 처리할 수도 있다 .

기다리기

import java.util.Queue;import java.util.LinkedList; public class RequestQueue { private final Queue<Request> queue = new LinkedList<Request>(); public synchronized Request getRequest() { while(queue.peek() == null) { try { wait(); } catch (InterruptedException e) { } } return queue.remove(); } public synchronized void putRequest(Request request) { queue.offer(request); notifyAll(); }}

while 을 사용하는 이유는 wait 상태에서 벗어난 후에도 조건을 확인해야하기 때문 !!!

import java.util.concurrent.BlockingQueue;import java.util.concurrent.LinkedBlockingQueue; public class RequestQueue { private final BlockingQueue<Request> queue = new LinkedBlockingQueue<Request>(); public Request getRequest() { Request req = null; try { req = queue.take(); } catch (InterruptedException e) { } return req; } public void putRequest(Request request) { try { queue.put(request); } catch (InterruptedException e) { } }}

BlockingQueue

ConcurrentLinkedQueue

BlockingQueue 가 아니지만 내부 데이터 구조를 세션으로 분할하여 멀티쓰레드 상황에서 수행능력을 향상시킨다 .

BlockingQueue

java.util.Queue interface 의 sub-interface

offer 나 poll 이 아닌 고유의 put, take 를 통해 block 기능을 제공한다 .

ArrayBlockingQueue

요소의 상한이 있는 배열 베이스의 BlockingQueue

DelayQueue

Delayed 객체를 보관 .

지정 시간이 지나기 전에 take 할 수 없고 오래된 요소부터 take 한다 .

LinkedBlockingQueue

LinkedList 에 대응하는 BlockingQueue

기다리다 지나가기

import java.util.Queue;import java.util.LinkedList; public class RequestQueue { private final Queue<Request> queue = new LinkedList<Request>(); public synchronized Request getRequest() { long start = System.currentTimeMillis(); while(queue.peek() == null) { long now = System.currentTimeMillis(); long rest = 10000 - (now - start); if (rest <= 0) { return; } wait(rest); } return queue.remove(); } public synchronized void putRequest(Request request) { queue.offer(request); notifyAll(); }}

10 초 이상 대기하였을 때 notify 가 들어오면 종료한다 .

Producer-Consumer

데이터 중심의 역할 분배 !!!• 응답성을 크게 향상시킬 수 있다 !

producer queuecon-

sumer

Data 저장

Data 저장

Data 저장

Data 확인

Data 확인

Data 확인

Data 확인

Data 확인

Data 획득

Data 획득

Data 처리

Data 처리

처리 알림

처리 알림

Data 생성

Data 생성

Data 생성

producer queuecon-

sumer

Data 저장

Data 저장

Data 저장

Data 확인

Data 확인

Data 획득

Data 획득

Data 처리

Data 처리

처리 알림

처리 알림

Data 생성

Data 생성

Data 생성

Data 확인

Data 획득 Data 처리

처리 알림

public class Table { private final String[] buffer; private int tail; private int head; private int count; public Table(int count) { this.buffer = new String[count]; this.head = 0; this.tail = 0; this.count = 0; } public synchronized void put(String cake) throws InterruptedException { System.out.println( Thread.currentThread().getName() + " puts " + cake); while (count >= buffer.length) { wait(); } buffer[tail] = cake; tail = (tail + 1) % buffer.length; count++; notifyAll(); }

public synchronized String take() throws InterruptedException { while (count <= 0) { wait(); } String cake = buffer[head]; head = (head + 1) % buffer.length; count--; notifyAll(); System.out.println( Thread.currentThread().getName() + " takes " + cake); return cake; }}

import java.util.Random; public class ProducerThread extends Thread { private final Random random; private final Table table; private static int id = 0; public ProducerThread(String name, Table table, long seed) { super(name); this.table = table; this.random = new Random(seed); } public void run() { try { while (true) { Thread.sleep(random.nextInt(1000));

String cake="[No."+nextId()+"]"; table.put(cake); } } catch (InterruptedException e)

{ } } private static synchronized int nex-

tId() { return id++; }}

import java.util.Random; public class ConsumerThread extends Thread { private final Random random; private final Table table; public ConsumerThread(String name, Table table, long seed) { super(name); this.table = table; this.random = new Random(seed); } public void run() { try { while (true) { String cake = table.take();

Thread.sleep(random.nextInt(1000));

} } catch (InterruptedException e)

{ } }}

상태 변화가 없는 읽기에는 Lock 걸 필요가 있을까 ??

  쓰기 읽기

쓰기 X X

읽기 X O

4 가지 경우 중 하나만 Lock 이 필요없지만… .

쓰기와 읽기가 같은 빈도로 발생하지 않는다 !

만약 쓰기 thread 보다 읽기 thread 의 처리가 무겁거나 쓰기 threa 보다 읽기 threa 의 빈도가 더 높은 경우라면

읽기끼리 lock 해제하여 수행 능력을 높일 수 있다 !!!!

Java 에서는 ReentrantReadWriteLock class 에서 ReadLock, WriteLock 을 제공

public class Table { private final String[] buffer; private int tail; private int head; private int count; private final ReadWriteLock lock = new ReentrantReadWriteLock(true /* fair */); private final Lock readLock = lock.readLock(); private final Lock writeLock = lock.writeLock(); public Table(int count) { this.buffer = new String[count]; this.head = 0; this.tail = 0; this.count = 0; } public void put(String cake) throws InterruptedException { writeLock.lock(); try { slowly(); buffer[tail] = cake; tail = (tail + 1) % buffer.length; count++; } finally { writeLock.unlock(); } }

public String take() throws InterruptedException { writeLock.lock(); try { return doTake(); } finally { writeLock.unlock(); } } private String doTake() { String cake = buffer[head]; head = (head + 1) % buffer.length; count--; return cake; } private void slowly() { try { Thread.sleep(50); } catch (InterruptedException e) { } } public int getSize(){ readLock.lock(); try { return count; } finally { readLock.unlock(); } }}

Consumer 의 위임

producer table client

Data 저장

Data 확인

Data 획득

Data 획득

Data 처리

Data 처리

처리 알림

처리 알림

Data 생성

con-sumer

Data 저장

Data 생성

Data 저장

Data 생성

Data 확인

작업 의뢰

작업 의뢰

접수 알림

접수 알림

import java.util.concurrent.Executors;import java.util.concurrent.ExecutorService; public class Main { public static final BlockingQueue<String> table = new LinkedBlockingQueue<String>(); public static void main(String[] args) { public static void main(String[] args) { new ProducerThread("mThread-1", 31415).start(); new ClientThread("Client-Thread").start(); }}

public class ClientThread extends Thread { public ClientThread(String name) { super(name); } public void run() { try { while(true){ if(Main.table.getSize()>0) { new ConsumerThread().start(); }else{ Thread.sleep(1000); } } }catch(RejectedExecutionException e) { System.out.println( Thread.currentThread().getName() + ":" + e); }catch(CancellationException e) { System.out.println( Thread.currentThread().getName() + ":" + e); }catch(InterruptedException e) { System.out.println( Thread.currentThread().getName() + ":" + e); } }}

Consumer Thread 가 직접 일을 받아 처리하지 않고 해당 작업을 받아서 Consumer thread 에게 분배하는 Thread 를 생성하자 .

public class ClientThread extends Thread { ExecutorService executor; public ClientThread(String name, ExecutorService executor) { super(name); this.executor = executor; } public void run() { try { while(true){ if(Main.table.getSize()>0) { executor.execute(new ConsumerThread()); }else{ Thread.sleep(1000); } } } catch (RejectedExecutionException e) { System.out.println( Thread.currentThread().getName()+":"+e); } catch (CancellationException e) { System.out.println( Thread.currentThread().getName()+":"+e); } catch (InterruptedException e) { System.out.println( Thread.currentThread().getName()+":"+e); } }}

import java.util.concurrent.Executors;import java.util.concurrent.ExecutorService; public class Main { public static void main(String[] args) { ExecutorService executor = Executors.newCachedThreadPool(); public static void main(String[] args) { new ProducerThread("MakerThread-1",31415).start(); new ClientThread("Cleint-Thread",executor).start(); }}

응답성을 높여 지연시간을 줄일 수 있다 .작업을 실행함과 동시에 작업 실행 여부를 Producer 에게 알려줄 수 있다 .

VS

작업 결과에 Return 값을 받을 수 없다 .요청이 들어올 때마다 Thread 가 생기기 때문에 메모리 부족이 발생할 수 있다 . Thread 로 실행하기 때문에 순차적으로 작업이 처리되지 않는다 .

Consumer 의 대기

public class ClientThread extends Thread { private final ExecutorService executorService;  public ClientThread( ExecutorService executorService) { this.executorService = executorService; } public void run() { try { while( !Thread.currentThread().isInterrupted()) { while(Main.table.getSize()>0){ ConsumerThread request = new ConsumerThread(getName()); executorService.execute(request); } } } catch (RejectedExecutionException e) { System.out.println(getName() + " : " + e); } }}

public class ConsumerThread extends Thread { private final String parentName; public ConsumerThread(String name) { parentName = name; } public void run() { try { String cake = Main.table.take(); if(cake != null) System.out.println( parentName + getName() + " eat " + cake); Thread.sleep(100); } catch (InterruptedException e) { } }}

import java.util.concurrent.BlockingQueue;import java.util.concurrent.Executors;import java.util.concurrent.ExecutorService;import java.util.concurrent.LinkedBlockingQueue; public class Main { public static final BlockingQueue<String> table = new LinkedBlockingQueue<String>(); public static void main(String[] args) { new ProducerThread("MakerThread-1", 31415).start(); ExecutorService executorService = Executors.newFixedThreadPool(5); try { new ClientThread(executorService).start(); } finally { //executorService.shutdown(); } }}

Thread-0Thread-1 eat [No.0]Thread-0Thread-2 eat [No.1]Thread-0Thread-3 eat [No.2]Thread-0Thread-4 eat [No.3]Thread-0Thread-5 eat [No.4]……

ExecutorService executorService=Executors.newFixedThreadPool(5);처음부터 Thread 5 개를 실행되어 더 이상 추가되거나 줄지 않는다 .

• Thread 를 돌려가면서 사용하면 Thread 를 새로 만들고 실행하는데 걸리는 시간을 줄여 성능을 향상시킬 수 있다 .

• 대신 Worker Thread 의 처리능력을 초과하는 Request 가 들어오는 경우 Channel 에 Request 가 늘어나게 되고 메모리 용량을 차지하게 된다 .

Future

응답성을 높여 지연시간을 줄일 수 있다 .작업을 실행함과 동시에 작업 실행 여부를 Producer 에게 알려줄 수 있다 .

VS

작업 결과에 Return 값을 받을 수 없다 .요청이 들어올 때마다 Thread 가 생기기 때문에 메모리 부족이 발생할 수 있다 . Thread 로 실행하기 때문에 순차적으로 작업이 처리되지 않는다 .

Consumer 의 위임

응답성을 높여 지연시간을 줄일 수 있다 .작업을 실행함과 동시에 작업 실행 여부를 Producer 에게 알려줄 수 있다 .

CPU 가 할당받지 않는 유후 시간이 존재하는 상황이라면 !!!

응답성 뿐 아니라 처리량 또한 비약적으로 향상된다 !!!

return 값을 준비하는 부분과 return 값을 이용하는 부분을 분리해서 구현해보자 !!

작업 결과에 Return 값을 받을 수 없다 ????

public class ConsumerThread extends Thread { public void run() { try { String cake = Main.table.take(); if(cake != null) System.out.println(getName() + " eat " + cake); Thread.sleep(100); } catch (InterruptedException e) { } }}

public class ConsumerClientThread extends Thread { ExecutorService service; public ConsumerClientThread(String name, ExecutorService executorService) { super(name); service = executorService; } public void run() { try { while(true){ /*while(Main.table.getSize()>0){*/ service.execute(new ConsumerThread()); Thread.sleep(200); /*}*/ } } catch (RejectedExecutionException e) { System.out.println(Thread.currentThread().getName() + ":" + e); } catch (CancellationException e) { System.out.println(Thread.currentThread().getName() + ":" + e); } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + ":" + e); } }}

public class ProducerThread implements Callable<String> { private final Random random; private static int id = 0; public ProducerThread(long seed) { this.random = new Random(seed); } public String call() { String cake="[No." + nextId() + "]"; try { Thread.sleep(random.nextInt(1000)); Main.table.put(cake); } catch (InterruptedException e) { } return cake; }  private static synchronized int nex-tId() { return id++; }}

public class ProducerClientThread extends Thread { ExecutorService service; public ProducerClientThread(String name, ExecutorService executorService) { super(name); service = executorService; } public void run() { try { while(true){ Future<String> future = service.submit(new ProducerThread(31415)); Thread.sleep(10); String value = future.get(); System.out.println(Thread.currentThread().getName() + ": value = " + value); } } catch (RejectedExecutionException e) { System.out.println(Thread.currentThread().getName() + ":" + e); } catch (CancellationException e) { System.out.println(Thread.currentThread().getName() + ":" + e); } catch (ExecutionException e) { System.out.println(Thread.currentThread().getName() + ":" + e); } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + ":" + e); } }}

import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class Main { public static final BlockingQueue<String> table = new LinkedBlockingQueue<String>(); public static void main(String[] args) { ExecutorService mExecutorService = Executors.newSingleThreadExecutor(); ExecutorService eExecutorService = Executors.newFixedThreadPool(5); try { new ProducerClientThread("mThread-1", mExecutorService).start(); new ProducerClientThread("mThread-2", mExecutorService).start(); new ConsumerClientThread("eThread", eExecutorService).start(); Thread.sleep(2000); } catch (InterruptedException e) { } finally { System.out.println("*** shutdown ***"); makeExecutorService.shutdown(); eatExecutorService.shutdown(); } }}

mThread-2: value = [No.0]Thread-0 eat [No.0]mThread-1: value = [No.1]Thread-1 eat [No.1]mThread-2: value = [No.2]Thread-2 eat [No.2]mThread-1: value = [No.3]Thread-3 eat [No.3]mThread-2: value = [No.4]Thread-4 eat [No.4]mThread-1: value = [No.5]Thread-5 eat [No.5]mThread-2: value = [No.6]Thread-6 eat [No.6]mThread-1: value = [No.7]Thread-7 eat [No.7]*** shutdown ***eThread:java.util.concurrent.RejectedExecutionException: Task Thread[Thread-10,5,main] rejected from java.util.concurren-t.ThreadPoolExecutor@7ec00b12[Shutting down, pool size = 3, active threads = 3, queued tasks = 0, completed tasks = 7]mThread-2: value = [No.8]Thread-8 eat [No.8]mThread-2:java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@2e85d205 rejected from java.util.concurrent.ThreadPoolExecutor@2bc62c46[Shutting down, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 9]mThread-1: value = [No.9]Thread-9 eat [No.9]mThread-1:java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@1dfec55f rejected from ja-va.util.concurrent.ThreadPoolExecutor@2bc62c46[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 10]

감사합니다 .