Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- jpa 활용2 - api 개발 고급
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch4
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch13
- 자바의 정석 기초편 ch8
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch12
- 스프링 mvc2 - 로그인 처리
- 스프링 mvc1 - 서블릿
- 스프링 mvc2 - 타임리프
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch5
- 스프링 mvc1 - 스프링 mvc
- 게시글 목록 api
- 자바 기본편 - 다형성
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch1
- 스프링 고급 - 스프링 aop
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch3
- @Aspect
- 자바의 정석 기초편 ch6
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch7
- 스프링 입문(무료)
- 2024 정보처리기사 시나공 필기
Archives
- Today
- Total
나구리의 개발공부기록
자바의 정석 기초편 ch13 - 33 ~ 36 [쓰레드의 동기화, wait(), notify()] 본문
유튜브 공부/JAVA의 정석 기초편(유튜브)
자바의 정석 기초편 ch13 - 33 ~ 36 [쓰레드의 동기화, wait(), notify()]
소소한나구리 2023. 12. 19. 11:561) 쓰레드의 동기화(synchronization)
- 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 막는 것
- 멀티 쓰레드 프로세스에서는 쓰레드의 작업이 다른쓰레드에 영향을 미칠 수 있음
- 진행중인 작업이 다른 쓰레드에게 간섭받지 않게하려면 '동기화'가 필요하며 동기화를 하려면 간섭받지 않아야 하는 문장들을 '임계 영역'으로 설정해야함
- 임계영역은 락(lock)을 얻은 단 하나의 쓰레드만 출입가능함(객체 1개에 락 1개)
(1) synchronized를 이용한 동기화
- synchronized로 임계영역(lock이 걸리는 영역)을 설정할 때 메서드 전체를 임계영역으로 설정하거나 특정영역을 임계영역으로 지정할 수 있음
- 임계영역은 한번에 한 쓰레드만 사용할 수 있기 때문에 영역을 최소하는 것이 좋으며 임계영역이 많을 수록 성능이 떨어짐
- 즉 가능한 임계영역의 개수를 최소한으로 좁게작성하는 것이 좋으므로 메서드 전체보단 특정한 영역을 임계 영역으로 지정하는것을 권장함
- 변수의 선언을 꼭 private로 해야 동기화의 의미가 있음(외부에서 사용 불가)
// 1. 메서드 전체를 임계 영역으로 설정 - 메서드 반환타입 앞에 synchronized를 붙임
public synchronized void calcSum() { // 임계영역 시작
//...
} // 임계영역 끝
// 2. 특정한 영역을 임계 영역으로 지정
synchronized(객체의 참조변수) { // 임계영역 시작
//...
} // 임계영역
(1) 예제 - 동기화 안된 ver.
- 잔돈을 출금하는 예제로 잔고에는 음수가 나오지 않을 것을 기대하고 프로그래밍을 하였으나 실행 결과 음수가 나옴
- 각 쓰레드가 run()메서드 작업 중 slepp()으로 인하여 1초동안 멈춘뒤 다시 실행함
- 이때 쓰레드들이 동기화가 되어있지 않았으므로 if문의 조건을 만족한 먼저 도착쓰레드가 sleep으로 설정한 대기 시간때문에 그다음 코드에서 잔액에서 출금액을 바로 제외하지 못할 때 다른 스레드가 if의 조건을 만족하여 sleep 상태에 빠짐
- 이 때 먼저 sleep상태에있던 쓰레드가 깨어나서 잔고에서 출금액을 빼가고 두번째 스레드가 깨어나서 다시 잔고에서 출금액을 빼갔을 때 잔고가 출금액보다 작다면 음수로 반환함
class Ex13_12 {
public static void main(String args[]) {
Runnable r = new RunnableEx12();
new Thread(r).start(); // ThreadGroup에 의해 참조되므로 gc대상이 아니다.
new Thread(r).start(); // ThreadGroup에 의해 참조되므로 gc대상이 아니다.
}
}
class Account {
private int balance = 1000;
public int getBalance() {
return balance;
}
public void withdraw(int money){
if(balance >= money) {
try { Thread.sleep(1000);} catch(InterruptedException e) {}
balance -= money;
}
} // withdraw
}
class RunnableEx12 implements Runnable {
Account acc = new Account();
public void run() {
while(acc.getBalance() > 0) {
// 100, 200, 300중의 한 값을 임으로 선택해서 출금(withdraw)
int money = (int)(Math.random() * 3 + 1) * 100;
acc.withdraw(money);
System.out.println("balance:"+acc.getBalance());
}
} // run()
}
예제- 동기화 ver.
- 실제 로직을 수행하는 메서드의 전체를 synchronized를 붙혀서 임계영역을 지정
- 임계영역의 메서드는 쓰레드가 동시에 동작하지 않기 때문에 1초동안 멈추고 잔고에서 출금액을 반환하고 잔고의 변수 값을 읽어오는 메서드가 싱글 쓰레드로 동작함
- 즉 하나의 쓰레드가 잔고에서 출금을 완료해야 다음 쓰레드가 잔고에서 출금을 할 수 있기 때문에 if문을 두 쓰레드가 동시에 만족할 수 없게되어 잔고는 음수가 나올 수 없음
class Ex13_13 {
public static void main(String args[]) {
Runnable r = new RunnableEx13();
new Thread(r).start();
new Thread(r).start();
}
}
class Account2 {
private int balance = 1000; // private으로 해야 동기화가 의미가 있다.
public synchronized int getBalance() { // 읽을 때도 동기화를 해줘야 됨
return balance;
}
public synchronized void withdraw(int money){ // synchronized로 메서드를 동기화
if(balance >= money) {
try { Thread.sleep(1000);} catch(InterruptedException e) {}
balance -= money;
}
} // withdraw
}
class RunnableEx13 implements Runnable {
Account2 acc = new Account2();
public void run() {
while(acc.getBalance() > 0) {
// 100, 200, 300중의 한 값을 임으로 선택해서 출금(withdraw)
int money = (int)(Math.random() * 3 + 1) * 100;
acc.withdraw(money);
System.out.println("balance:"+acc.getBalance());
}
} // run()
}
2) wait() 와 notify()
- 동기화를 하면 한번에 한개의 쓰레드만 사용 가능하기 때문에 효율이 떨어짐
- 효율(성능)을 높이기 위해 wait()와 notify()를 사용
- wait() - 객체의 lock을 풀고 쓰레드를 해당 객체의 waiting pool에 넣음
- notify() - waiting pool에서 대기중인 쓰레드 중의 하나를 깨움 (대기중인 쓰레드를 랜덤하게 깨우기 때문에 운이 나쁘면 특정 쓰레드가 계속 대기 상태로 있을 수 있음)
- notifyAll() - waiting pool에서 대기중인 모든 쓰레드를 깨움(일반적으로 더 좋음)
(1) 출금 메서드와 입금 메서드에 동기화
- 출금, 입금 메서드를 synchronized로 동기화
- 잔고가 출금액보다 적으면 반복문으로 잔고가 출금액보다 많아질 때까지 해당 스레드를 계속 대기시킴(깨워도 다시 반복문으로 다시 대기상태로 만듦)
- wait()를 호출하여 출금메서드에서 동작하던 쓰레드를 waiting pool로 이동시켜 입금 메서드를 작동(입금 메서드가 동기화 접근 권한을 받음)
- 입금을 완료하면 notify()로 대기중인 쓰레드를 깨워서 일어난 쓰레드가 멈췄던 작업을 계속 수행함
(2) 예제의 구조
- 요리사(쓰레드1)는 Table에 음식을 추가, 손님(쓰레드2)은 Table의 음식을 소비하는 프로그래밍 작성
- 요리사와 손님이 같은 객체(Table)을 공유하기 때문에 동기화가 필요함(동기화 하지 않으면 예외 발생)
- [예외 상황1] - 요리사가 Table에 요리를 추가하는 과정에 손님이 요리를 먹어버려서 컬렉션을 작업을 두 쓰레드가 동시에 작업을 하여 ConcurrentModificationException이 발생함
- [예외 상황2] - 하나 남은 요리를 손님2가 먹으려하는데, 손님1이 먹어버려서 IndexOutOfBoundsException이 발생함
(3) 예제 - 동기화만 진행ver.
- 동기화로 예외를 해결 하였으나 더이상 작업이 진행 되지 않음
- 예외는 발생하지 않지만 손님이 Table에 먹을 음식이 없으면 해당 메서드에서 빠져나오지 못하고 계속 코드를 수행함
- 요리사가 Table의 lock을 얻을 수 없어서 음식을 추가하지 못하게 되어 더이상 프로그램이 실행되지 않음
import java.util.ArrayList;
class Customer implements Runnable {
private Table table;
private String food;
Customer(Table table, String food) {
this.table = table;
this.food = food;
}
public void run() {
while(true) {
try { Thread.sleep(10);} catch(InterruptedException e) {}
String name = Thread.currentThread().getName();
if(eatFood())
System.out.println(name + " ate a " + food);
else
System.out.println(name + " failed to eat. :(");
} // while
}
boolean eatFood() { return table.remove(food); }
}
class Cook implements Runnable {
private Table table;
Cook(Table table) { this.table = table; }
public void run() {
while(true) {
int idx = (int)(Math.random()*table.dishNum());
table.add(table.dishNames[idx]);
try { Thread.sleep(100);} catch(InterruptedException e) {}
} // while
}
}
class Table {
String[] dishNames = { "donut","donut","burger" };
final int MAX_FOOD = 6;
private ArrayList<String> dishes = new ArrayList<>();
public synchronized void add(String dish) { // synchronized를 추가
if(dishes.size() >= MAX_FOOD)
return;
dishes.add(dish);
System.out.println("Dishes:" + dishes.toString());
}
public boolean remove(String dishName) {
synchronized(this) {
while(dishes.size()==0) {
String name = Thread.currentThread().getName();
System.out.println(name+" is waiting.");
try { Thread.sleep(500);} catch(InterruptedException e) {}
}
for(int i=0; i<dishes.size();i++)
if(dishName.equals(dishes.get(i))) {
dishes.remove(i);
return true;
}
} // synchronized
return false;
}
public int dishNum() { return dishNames.length; }
}
class Ex13_14 {
public static void main(String[] args) throws Exception {
Table table = new Table(); // 여러 쓰레드가 공유하는 객체
new Thread(new Cook(table), "COOK").start();
new Thread(new Customer(table, "donut"), "CUST1").start();
new Thread(new Customer(table, "burger"), "CUST2").start();
Thread.sleep(5000);
System.exit(0);
}
}
(4) 예제 - wait(), notify() 사용 ver.
- [문제점] 음식이 없을 때 손님이 Table의 lock을 계속 가지고 있어서 요리사가 lock을 얻지 못해 Table에 음식을 추가 못함
- [해결책] 음식이 없을 때 wait()으로 손님이 lock을 풀고 대기상태로 만들어 요리사가 lock을 획득하여 음식을 추가하고 lock을 반납 후 notify()로 손님에게 알려서 대기하던 스레드를 호출하여 프로그램을 가동
- 단, wait()와 notify()가 어떤 쓰레드에게 적용 되는지 불분명한데 이를 해결하기 위해 Lock & Condition을 사용하면 됨(자바의 정석 3판 - 필요하면 검색해서 찾아보기)
- wait(), notify()로 동기화가된 정상적인 멀티쓰레스 프로그래밍이 가능해짐
import java.util.ArrayList;
class Customer2 implements Runnable {
private Table2 table;
private String food;
Customer2(Table2 table, String food) {
this.table = table;
this.food = food;
}
public void run() {
while(true) {
try { Thread.sleep(100);} catch(InterruptedException e) {}
String name = Thread.currentThread().getName();
table.remove(food);
System.out.println(name + " ate a " + food);
} // while
}
}
class Cook2 implements Runnable {
private Table2 table;
Cook2(Table2 table) { this.table = table; }
public void run() {
while(true) {
int idx = (int)(Math.random()*table.dishNum());
table.add(table.dishNames[idx]);
try { Thread.sleep(10);} catch(InterruptedException e) {}
} // while
}
}
class Table2 {
String[] dishNames = { "donut","donut","burger" }; // donut의 확률을 높인다.
final int MAX_FOOD = 6;
private ArrayList<String> dishes = new ArrayList<>();
public synchronized void add(String dish) {
while(dishes.size() >= MAX_FOOD) {
String name = Thread.currentThread().getName();
System.out.println(name+" is waiting.");
try {
wait(); // COOK쓰레드를 기다리게 한다.
Thread.sleep(500);
} catch(InterruptedException e) {}
}
dishes.add(dish);
notify(); // 기다리고 있는 CUST를 깨우기 위함.
System.out.println("Dishes:" + dishes.toString());
}
public void remove(String dishName) {
synchronized(this) {
String name = Thread.currentThread().getName();
while(dishes.size()==0) {
System.out.println(name+" is waiting.");
try {
wait(); // CUST쓰레드를 기다리게 한다.
Thread.sleep(500);
} catch(InterruptedException e) {}
}
while(true) {
for(int i=0; i<dishes.size();i++) {
if(dishName.equals(dishes.get(i))) {
dishes.remove(i);
notify(); // 잠자고 있는 COOK을 깨우기 위함
return;
}
} // for문의 끝
try {
System.out.println(name+" is waiting.");
wait(); // 원하는 음식이 없는 CUST쓰레드를 기다리게 한다.
Thread.sleep(500);
} catch(InterruptedException e) {}
} // while(true)
} // synchronized
}
public int dishNum() { return dishNames.length; }
}
class Ex13_15 {
public static void main(String[] args) throws Exception {
Table2 table = new Table2();
new Thread(new Cook2(table), "COOK").start();
new Thread(new Customer2(table, "donut"), "CUST1").start();
new Thread(new Customer2(table, "burger"), "CUST2").start();
Thread.sleep(2000);
System.exit(0);
}
}
** 출처 : 남궁성의 정석코딩_자바의정석_기초편 유튜브