3. 한발 더 나아가 미래를 내다보는 개발자는 이미 단일 컴퓨터
내부에서의 병렬 처리를 넘어 여러 대의 컴퓨터에서 병렬로
동작하는 플랫폼을 찾고 있고, 이와 같은 분산 병렬 처리 플랫
폼이 한창 인기를 얻는 요즘입니다. 이런 대규모 병렬 처리 플
랫홈도 중요하긴 하지만, 그 안에서는 항상 단일 프로세스 내
부에서 동작하는 여러 스레드가 안정적으로 실행되도록 하는
병렬 처리 기법이 적용되어 있음을 잊어서는 안될 것입니다.
-’멀티 코어를 100% 활용하는 자바 병렬 프로그래밍’ 옮김이의 말 중
9. 원하는 일이 원하는 시기에 동작한다.
Deadlock
서로 다른 Thread가 서로 가지고 있는 독점 자원을 원하여 멈춰져 있는 현상
Starvation
Thread가 자신이 원하는 자원을 할당받지 못하는 상황
Livelock
서로 다른 Thread가 서로의 상태를 변경하면서 작업을 진행하지 못하는 현상
11. Muti-Thread 환경은 Single-Thread 환경보다 성능상 자원을 더 소모한다.
(락, 메모리, 상태 설정, Thread 생성 및 제거 등등)
Muti-thread는 결국!!
이런 비용을 지불하여 응답성과 처리용량을 늘리는 것이 목적!!!
서로 다른 Thread의 경쟁할 일이 없는 synchonized 블록의 경우 JVM 수준에서 동
기화 관련 부하를 줄이거나 아예 없애주기 때문에 매우 빠르게 동작한다. 실행 순
서를 고정하는 역할을 할 수도 있다.
대신 경쟁조건이 존재하는 경우 대부분의 JVM이 운영체제의 기능을 호출한다.
13. Synchonized 키워드, volatile 키워드
• 성능을 위해서는 위에 두 키워드 뿐 아니라 Multi-thread의 기법에 들
어가는 모든 기술을 최소화하는 것이 좋다.
하지만!! 이런 키워드가 없는 경우
• 후임 개발자가 해당 Class에 대해 Thread safe 여부를 판단하기 힘들고
더 심한 경우에는 이미 안전한 Class를 망칠 수도 있다.
15. 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 : BEGIN
Person : END
time = 475msec.
SyncPerson : BEGIN
SyncPerson : END
time = 523msec.
16. 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 10000000
Single Thread Person : END time = 534msec.
Main Person : BEGIN
Thread-0 Person : BEGIN 5000000
Thread-1 Person : BEGIN 5000000
Thread-0 Person : END time = 297msec.
Thread-1 Person : END time = 298msec.
Main Person : END time = 298msec.
Single Thread SyncPerson : BEGIN 10000000
Single Thread SyncPerson : END time = 668msec.
Main SyncPerson : BEGIN
Thread-4 SyncPerson : BEGIN 5000000
Thread-5 SyncPerson : BEGIN 5000000
Thread-4 SyncPerson : END time = 1110msec.
Thread-5 SyncPerson : END time = 1121msec.
Main SyncPerson : END time = 1790msec.
17. 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());
}
bob
pop
bob
String의 replace()를 사용하면 실제 객체 값에는 영향을
주지 않고 그 순간만 데이터를 바꿔서 사용할 수 있다.
19. 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 = bob
Person :: name = pop
StringBuffer에서 제공하는 replace()는 String과 달리
객체에 직접 영향을 주기 때문에 Muti-thread 환경에서
안전하지 못하다.
21. Thread의 종료는 매우 중요하다!!
• Thread가 정상적으로 종료하지 않으면,
-> JVM 역시 종료되지 않고 대기하게 된다.
22. 실행 중인 Thread를 그냥 종료???
• 작업 중이던 메모리에 대한 반환 작업이 정상적으로 이뤄지지 않는
경우도 있고 해서 Thread.stop()과 Thread.suspend()와 같은 메소드는
사용하면 안된다!!!(deprecated)
JAVA에서는 특정 Thread를 종료시키면 안된다!!
• 작업을 수행하는 Thread와 해당 Thread의 종료를 요청하는 Thread를
같이 생각해야 한다.
23. 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는 작업을
정리한 후에 종료한다.
25. 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가 아무런 영향도 미치지 못한다.
26. 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);
}
}
}
}
27. 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 메소드를 사용하자.
29. Thread 상태 변화 다이어그램
초기상태
실행
실행 가능
실행 상태
종료
sleep
TIME_WATING
join
TIME_WATING
wait
TIME_WATING
BLOCKED
Thread 생성
Start 메소드
schedule
Run 메소드 종료
GC
sleep
join
wait
Synchronized
I/O 대기 등..
interrupt
join 종료
time out
interrupt
notify/notifyAll
time out
interrupt
sleep 종료
time out
Lock 획득
30. notify는 wait set에서 대기하는 thread 중 하나만 실행하고
notifyAll은 wait set에 들어가 있는 모든 thread를 실행한다.
경쟁에서 밀린 thread는 다시 wait set으로 들어가게 된다.
31. 그렇지만 notify가 더 좋다고 할 수는 없다.
우선 notify가 깨우는 thread는 JVM과 운영체제가 결정하고
어떤 thread가 동작하는지 알 수가 없다. 그리고…
32. 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는 동작하지 않는다.
37. 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으로 처리할 수도 있다.
42. ConcurrentLinkedQueue
BlockingQueue가 아니지만 내부 데이터 구조를 세
션으로 분할하여 멀티쓰레드 상황에서 수행능력을
향상시킨다.
BlockingQueue
java.util.Queue interface의 sub-interface
offer나 poll이 아닌 고유의 put, take를 통해 block
기능을 제공한다.
ArrayBlockingQueue
요소의 상한이 있는 배열 베이스의 BlockingQueue
LinkedBlockingQueue
LinkedList에 대응하는 BlockingQueue
DelayQueue
Delayed 객체를 보관.
지정 시간이 지나기 전에 take할 수 없고 오래된 요
소부터 take한다.
47. producer queue consumer
Data 저장
Data 저장
Data 저장
Data 확인
Data 확인
Data 획득
Data 확인
Data 확인
Data 확인
Data 획득
Data 처리
처리 알림
Data 처리
처리 알림
Data 생성
Data 생성
Data 생성
48. producer queue consumer
Data 저장
Data 저장
Data 저장
Data 확인
Data 획득
Data 확인
Data 획득
Data 처리
처리 알림
Data 처리
처리 알림
Data 생성
Data 생성
Data 생성
Data 확인
Data 획득 Data 처리
처리 알림
49. 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;
}
}
50. 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 nextId() {
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) {
}
}
}
52. 쓰기 읽기
쓰기 X X
읽기 X O
4가지 경우 중 하나만 Lock이 필요없지만….
53. 쓰기와 읽기가 같은 빈도로 발생하지 않는다!
만약 쓰기 thread보다 읽기 thread의 처리가 무겁거나
쓰기 threa보다 읽기 threa의 빈도가 더 높은 경우라면
읽기끼리 lock 해제하여 수행 능력을 높일 수 있다!!!!
Java에서는 ReentrantReadWriteLock class에서 ReadLock, WriteLock을 제공
54. 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();
}
}
}
56. producer table client
Data 저장
Data 확인
Data 획득
Data 획득
Data 처리
처리 알림
Data 처리
처리 알림
Data 생성
consumer
Data 저장
Data 생성
Data 저장
Data 생성
Data 확인
작업 의뢰
접수 알림
작업 의뢰
접수 알림
57. 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를 생성하자.
58. 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();
}
}
59. 응답성을 높여 지연시간을 줄일 수 있다.
작업을 실행함과 동시에 작업 실행 여부를 Producer에게 알려줄 수 있다.
VS
작업 결과에 Return 값을 받을 수 없다.
요청이 들어올 때마다 Thread가 생기기 때문에 메모리 부족이 발생할 수 있다.
Thread로 실행하기 때문에 순차적으로 작업이 처리되지 않는다.
65. 응답성을 높여 지연시간을 줄일 수 있다.
작업을 실행함과 동시에 작업 실행 여부를 Producer에게 알려줄 수 있다.
VS
작업 결과에 Return 값을 받을 수 없다.
요청이 들어올 때마다 Thread가 생기기 때문에 메모리 부족이 발생할 수 있다.
Thread로 실행하기 때문에 순차적으로 작업이 처리되지 않는다.
66. 응답성을 높여 지연시간을 줄일 수 있다.
작업을 실행함과 동시에 작업 실행 여부를 Producer에게 알려줄 수 있다.
CPU가 할당받지 않는 유후 시간이 존재하는 상황이라면!!!
응답성 뿐 아니라 처리량 또한 비약적으로 향상된다!!!
67. return 값을 준비하는 부분과
return 값을 이용하는 부분을
분리해서 구현해보자!!
작업 결과에 Return 값을 받을 수 없다????