쓰레드의 동기화(synchronization)
- 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것
- 멀티 쓰레드 프로세스에서는 다른 쓰레드의 작업에 영향을 미칠 수 있다. -> 진행 중인 작업이 다른 쓰레드에게 간섭 받지 않게 하려면 ‘동기화’가 필요
- 동기화하려면 간섭 받지 않아야 하는 문장들을 ‘임계 영역’으로 설정
- 임계영역은 락(lock)을 얻은 단 하나의 쓰레드만 출입가능(객체 1개에 락 1개)
synchronized를 이용한 동기화
- synchronized로 임계영역(lock이 걸리는 영역)을 설정하는 방법 2가지
-> 한 번에 한 쓰레드만 사용할 수 있기 때문에 영역을 최소화 해야함
1번은 synchronized메서드가 호출된 시점부터 해당 메서드가 포함된 객체의 lock을 얻어 작업을 수행하다가 메서드가 종료되면 lock을 반환
2번은 메서드 내의 코드 일부를 블럭 { }으로 감싸고 블럭 앞에 ‘synchronized(참조변수)’를 붙이는 것인데, 이 때 참조변수는 락을 걸고자 하는 객체를 참조하는 것이어야 한다.
-> 이 블록은 synchronized블록의 영역안으로 들어가면서 쓰레드는 지정된 객체의 lock을 얻게 되고 블록을 벗어나면 lock 반환
예제) 은행계좌(account)에서 잔고(balance)를 확인하고 임의의 금액을 출금(withdraw)하는 예제
public class ThreadEx21 {
public static void main(String[] args) {
Runnable r = new RunnableEx21(); // RunnableEx21이 Runnable인터페이스 구현
// Thread th = new Thread(r);
// th.start();
new Thread(r).start(); // ThreadGroup에 의해 참조되므로 gc대상이 아니다. 위의 두 줄을 한 줄로
}
}
class Account { // 현재 계좌에 있는 금액
private int balance = 1000; // private으로 해야 동기화가 의미가 있다.
public synchronized int getBalance() { // 읽고 쓰는 객체에도 동기화!
return balance;
}
public synchronized void withdraw(int money) { // 출금할 때 방해받지 않도록 동기화
if(balance >= money) { // 계좌에 있는 금액이 빼려는 돈보다 크면 출금 가능
try { Thread.sleep(1000); } catch (InterruptedException e) { }
balance -= money;
}
}// withdraw
}
class RunnableEx21 implements Runnable { //Runnable 인터페이스 구현!
Account acc = new Account(); // 계좌 객체 생성
public void run() {
while(acc.getBalance()>0) { // 현재 잔고가 0보다 크면
//100, 200, 300중의 한 값을 임의로 선택해서 출금(withdraw)
int money = (int)(Math.random()*3+1)*100;
acc.withdraw(money);
System.out.println("balance:" + acc.getBalance());
}
}//run()
}
wait( )과 notify( )
- synchronized로 동기화해서 공유 데이터를 보호하는 것이 좋지만, 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록 하는 것도 중요
- 동기화의 효율을 높이기 위해 wait( ), notify( )를 사용 (기다리기/통보, 알려주기)
- Object클래스에 정의되어 있으며, wait( )과 notify( )는 동기화 블록 내에서만 사용할 수 있다.
wait( ) | 객체의 lock을 풀고 쓰레드를 해당 객체의 waiting pool에 넣는다. |
notify( ) | waiting pool에서 대기중인 쓰레드 중의 하나를 깨운다. |
notifyAll( ) | waiting pool에서 대기중인 모든 쓰레드를 깨운다. |
-> 동기화된 임계영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, 일단 wait( )을 호출해 쓰레드가 락을 반납하고 기다림
-> 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 수행
-> 나중에 작업을 진행할 수 있는 상황이 되면 notify( )를 호출해서, 작업을 중단했던 쓰레드가 다시 락을 얻어 작업을 진행할 수 있게 함
- wait( )은 notify( ) 또는 notifyAll( )이 호출될 까지만 기다리지만, 매개변수가 있는 wait( )은 지정된시간 동안만 기다린다.
예제)
1. Thread를 상속받는 ThreadB클래스를 작성
class ThreadB extends Thread {
// 해당 쓰레드가 실행되면 자기 자신의 모니터링 락을 획득
// 5번 반복하면서 0.5초씩 쉬면서 total에 값을 누적
// 그 후에 nofity( )메서드를 호출해 wait하고 있는 쓰레드를 깨움
int total = 0;
public void run() {
synchronized(this) {
for(int i=0; i<5; i++) {
System.out.println(i + "를 더합니다.");
total += i;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
} notify();
} // synchronized
} // run()
}
2. ThreadB를 사용하며 wait하는 클래스 작성
public class ThreadA {
public static void main(String[] args) {
// 앞에서 만든 쓰레드B를 만든 후 start
// 해당 쓰레드가 실행되면, 해당 쓰레드는 run메서드 안에서 자신의 모니터링 락을 획득
ThreadB b = new ThreadB();
b.start();
// b에 대해 동기화 블럭을 설정
/* 만약 main쓰레드가 아래의 블록을 위의 Thread보다 먼저 실행되었다면
wait을 하게 되면서 모니터링 락을 놓고 대기*/
synchronized(b) {
try {
// b.wait( )메서드를 호출
// 메인쓰레드는 정지
// ThreadB가 5번 값을 더한 후 notify를 호출하게 되면 wait에서 main쓰레드가 깨어남
System.out.println("b가 완료될때까지 기다립니다.");
b.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
// 깨어난 후 결과를 출력
System.out.println("Total is: " + b.total);
}//synchronized
}//main
}
'멀티캠퍼스 풀스택 과정 > Java의 정석' 카테고리의 다른 글
자바의 정석11-2 java.util.Function 패키지의 함수형 인터페이스와 메서드 참조 (0) | 2022.01.16 |
---|---|
자바의 정석11-1 람다와 함수형인터페이스 (0) | 2022.01.16 |
자바의 정석10-4 데몬 쓰레드(daemon thread)와 쓰레드의 실행제어 (0) | 2022.01.13 |
자바의 정석10-3 쓰레드의 우선순위와 쓰레드 그룹 (0) | 2022.01.13 |
자바의 정석10-2 싱글쓰레드와 멀티쓰레드, 쓰레드의 I/O 블락킹 (0) | 2022.01.12 |