본문 바로가기
멀티캠퍼스 풀스택 과정/Java의 정석

자바의 정석10-5 쓰레드의 동기화(Synchronization)

by 이쟝 2022. 1. 13.

쓰레드의 동기화(synchronization)

- 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것

- 멀티 쓰레드 프로세스에서는 다른 쓰레드의 작업에 영향을 미칠 수 있다. -> 진행 중인 작업이 다른 쓰레드에게 간섭 받지 않게 하려면 ‘동기화’가 필요

 

- 동기화하려면 간섭 받지 않아야 하는 문장들을 임계 영역으로 설정

- 임계영역은(lock)을 얻은 단 하나의 쓰레드만 출입가능(객체 1개에 락 1)


synchronized를 이용한 동기화

- synchronized로 임계영역(lock이 걸리는 영역)을 설정하는 방법 2가지

 

 

-> 한 번에 한 쓰레드만 사용할 수 있기 때문에 영역을 최소화 해야함

1synchronized메서드가 호출된 시점부터 해당 메서드가 포함된 객체의 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
}

 

더보기
더보기

<출력값>

b 완료될떄까지 기다립니다.

0 더합니다.

1 더합니다.

2 더합니다.

3 더합니다.

4 더합니다.

Total is: 10