일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- string
- geometry
- simulation
- array
- Matrix
- two pointers
- Data Structure
- 코테
- java
- Method
- Tree
- Class
- bit manipulation
- Binary Search
- Math
- dynamic programming
- Binary Tree
- Stack
- 구현
- SQL
- 코딩테스트
- 파이썬
- database
- sorting
- implement
- 자바
- Counting
- Number Theory
- hash table
- greedy
- Today
- Total
코린이의 소소한 공부노트
쓰레드의 동기화와 실행제어 본문
[동기화가 필요한 이유]
1. 멀티 쓰레드 프로세스에서는 다른 쓰레드의 작업에 영향을 미칠 수 있다.
- 진행 중인 작업이 다른 쓰레드에게 간섭받지 않게 하려면 동기화가 필요하다.
2. 동기화를 하려면 다른 쓰레드에게 간섭받지 않아야 하는 문장들을 임계 영역(critical section)으로 설정한다.
- 임계 영역은 락(lock)을 얻은 단 하나의 쓰레드만 출입이 가능하다.(객체 1개에 락 1개)
- 락을 걸어둠으로써 데이터의 일관성을 유지하게 한다.
- 임계 영역이 많을수록 성능이 떨어지기 때문에 최소한의 영역만 설정한다.
[synchronized 키워드를 이용한 동기화]
1. 임계 영역을 설정하는 방법
// 1) 특정한 영역을 임계 영역으로 지정
synchronized(객체의 참조변수) { // 임계 영역 시작
// ...
} // 임계 영역 끝
// 2) 메서드 전체를 임계 영역으로 지정
public synchronized void method() { // 임계 영역 시작
// ...
} // 임계 영역 끝
2. 이해를 돕기 위한 예시
class Account{
private int balance = 1000; // private를 해야 동기화가 의미가 있다.
public int getBalance(){
return balance;
}
public void withdraw(int money){ // 동기화가 되지 않은 상태
if(balance>=money){
try{ Thread.sleep(1000); } catch(InterruptedException e) { }
balance -= money;
}
} // 이론상으로는 잔액이 음수가 나올 수 없다.
}
class RunnableEx implements Runnable{
Account acc = new Account();
public void run(){
while(acc.getBalance()>0){
int money = (int)(Math.random()*3+1)*100; // 100, 200, 300중 하나
acc.withdraw(money);
System.out.println(“잔액 = ” + acc.getBalance());
}
}
}
// 실행 결과
잔액= 700
잔액= 700
잔액= 400
잔액= 200
잔액= 200
잔액= 0
잔액= -100 // 동기화가 되어 있지 않아 두 쓰레드가 동시에 잔액을 인출해서 음수가 나온 상황
// 동기화: Account 클래스 -> public synchronized void withdraw(int money)
잔액= 800
잔액= 700
잔액= 500
잔액= 400
잔액= 200
잔액= 0
잔액= 0 // 동기화가 되어있어 다른 쓰레드에 의해 잔액이 0이 됐을 때 인출하지 않고 잔액만 출력
[쓰레드의 실행제어]
1. 동기화의 효율을 높이기 위해 wait(), notify()를 사용한다.
- Object 클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있다.
2. wait() – 객체의 lock을 풀고 해당 객체의 쓰레드를 waiting pool에 넣는다.
3. notify() – waiting pool에서 대기 중인 쓰레드중의 하나를 깨운다.
4. notifyAll() – waiting pool에서 대기 중인 모든 쓰레드를 깨운다.
- notify()를 쓸 경우 계속 통지를 못 받는 쓰레드가 생길 수 있으므로, 일단 모두 깨운 후 그중 하나에게 락을 획득시킨다.
5. wait()와 notify()를 쓸 때, 누굴 기다리게 하고 누굴 깨우는지 코드를 면밀히 읽지 않는 이상 헷갈린다.
// 예시1 - 위에서 본 코드
class Account{
int balance = 1000;
public synchronized void withdraw(int money){
while(balance<money){ // 출금할 돈이 없다면
try{
wait(); // 대기 - 락을 풀고 기다리다가 통지를 받으면 락을 재획득(reentrance)한다.
} catch(InterruptedException e) { }
}
balance -= money;
}
public synchronized void deposit(int money){
balance += money; // 락을 얻은 쓰레드가 입금을 한다.
notify(); // 통지 - 대기중인 쓰레드 중 하나에게 입급됐다고 알린다.
}
}
// 예시2 - 요리사 vs 고객
import java.util.ArrayList;
class Table { // 음식이 놓일 테이블
String[] dishNames = { "donut","donut","burger" }; // donut의 확률을 높인다.
final int MAX_FOOD = 6;
private ArrayList<String> dishes = new ArrayList<>(); // ArryaList는 동기화X
public synchronized void add(String dish) { // 요리사가 테이블이 음식을 추가
while(dishes.size() >= MAX_FOOD) { // 접시가 최대치보다 많다면
String name = Thread.currentThread().getName();
System.out.println(name+" is waiting.");
try {
wait(); // 요리사를 기다리게 한다.
Thread.sleep(500);
} catch(InterruptedException e) {}
}
dishes.add(dish);
notify(); // 기다리고 있는 고객을 깨운다.
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(); // 고객을 기다리게 한다.
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(); // 요리사를 깨운다.
return; // 먹고 끝난다.
}
} // for문의 끝
try { // 원하는 음식이 없다면
System.out.println(name+" is waiting.");
wait(); // 원하는 음식이 나올 때까지 고객을 기다리게 한다.
Thread.sleep(500);
} catch(InterruptedException e) {}
} // while(true)
} // synchronized
}
public int dishNum() { return dishNames.length; } // 이 문제의 경우에는 3
}
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]); // { "donut","donut","burger" } 중의 하나 추가
try { Thread.sleep(10);} catch(InterruptedException e) {}
} // while
}
}
class Customer implements Runnable { // 고객
private Table table;
private String food;
Customer(Table table, String food) {
this.table = table;
this.food = food; // 이 문제의 경우 도넛 or 버거
}
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
}
}
// main()
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(2000); // 20초동안 메인쓰레드는 일시정지
System.exit(0);
// 결과
Dishes:[donut] // 최대 6접시가 될 때까지 add() 호출
Dishes:[donut, donut]
Dishes:[donut, donut, donut]
Dishes:[donut, donut, donut, donut]
Dishes:[donut, donut, donut, donut, donut]
Dishes:[donut, donut, donut, donut, donut, donut]
COOK is waiting. // 6접시가 다 차서 요리사는 대기중
CUST2 is waiting. // 원하는 음식(버거)이 없어 대기중
CUST1 ate a donut // 원하는 음식(도넛)을 먹음
Dishes:[donut, donut, donut, donut, donut, donut] // 고객이 1접시를 먹어 요리사가 1접시를 더 채움
CUST2 is waiting. // 원하는 음식(버거)이 없어 대기중
COOK is waiting. // 6접시가 다 차서 요리사는 대기중
CUST1 ate a donut // 원하는 음식(도넛)을 먹음
CUST2 is waiting. // 원하는 음식(버거)이 없어 대기중
CUST1 ate a donut // 원하는 음식(도넛)을 먹음
// 20초가 끝나 종료됨
'Java' 카테고리의 다른 글
함수형 인터페이스의 정의와 활용 (0) | 2022.12.29 |
---|---|
람다식의 정의와 작성 방법 (0) | 2022.12.28 |
데몬 쓰레드와 쓰레드의 실행제어, 상태 (0) | 2022.12.09 |
쓰레드의 우선순위와 그룹 (0) | 2022.11.30 |
싱글쓰레드와 멀티쓰레드의 실행 (0) | 2022.11.30 |