Java & Kotlin

[Java 기능] 동기화 구현 (1) - 리소스 하나를 공유하는 경우

Sue 2022. 2. 18. 20:35

critical section과 semaphore

  • critical section은 두개 이상의 쓰레드가 동시에 접근하는 경우 문제가 발생할 수 있기 때문에 방지해야 함
  • semaphore는 특별한 형태의 시스템 객체이며 get/release 기능이 있음
  • 한번에 오직 하나의 쓰레드만이 semaphore를 얻을 수 있고, 다른 쓰레드는 대기 상태(blocking)가 됨
  • semaphore를 가진 쓰레드만이 critical section에 진입할 수 있음 (일종의 열쇠 역할)

 

예제 코드

한 계좌에서 동시에 입금과 출금이 일어날 때(두개의 쓰레드가 하나의 공유 자원을 동시에 사용할 때) 결과 확인해보기 

 

동기화 구현하지 않은 경우
  • shared resource는 money이지만 자바에서는 객체로 resource 단위를 설정하기때문에 Bank의 인스턴스를 static으로 생성
  • park과 pWife 쓰레드는 각각 입금과 출금을 수행함
  • 수행 후 각 쓰레드가 반환한 공유 변수의 값을 확인
package ch22;

class Bank {
	
	private int money = 10000;
	
	public void deposit(int save) {
		
		int m = getMoney();
		
		try {
			Thread.sleep(3000);	// 3초 소요
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		setMoney(m + save);
	}
	
	public void withdrawl(int minus) {
		
		int m = getMoney();
		
		try {
			Thread.sleep(2000);	// 2초 소요
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		setMoney(m - minus);
	}

	public int getMoney() {
		return money;
	}

	public void setMoney(int money) {
		this.money = money;
	}
	
}

class Park extends Thread{
	
	public void run() {
		
		System.out.println("start deposit");
		SyncMain.myBank.deposit(3000);
		System.out.println("deposit(3000): " + SyncMain.myBank.getMoney());
	}
}

class ParkWife extends Thread{
	
	public void run() {
		
		System.out.println("start withdrawl");
		SyncMain.myBank.withdrawl(1000);
		System.out.println("withdrawl(1000): " + SyncMain.myBank.getMoney());
	}
}


public class SyncMain {
	
	public static Bank myBank = new Bank();	// 공유 변수가 선언되어 있는 객체를 static으로 생성

	public static void main(String[] args) {
		
		Park park = new Park();
		park.start();
		
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		ParkWife pWife = new ParkWife();
		pWife.start();
	}

}

 

수행 결과
  • deposit이 시작하고 끝나기 전에 (수행 중일때) withdrawl이 수행되었음
  • 각 쓰레드가 같은 공유변수 값을 가져와서 각자 연산 수행 후 반환
  • 이를 방지하기 위해 deposit과 withdrawl이 동시에 수행되지 않도록 구현해야 함
start deposit
start withdrawl
withdrawl(1000): 9000
deposit(3000): 13000

 


동기화 (Synchronization)

  • 두개의 쓰레드가 같은 객체에 접근하는 경우, 동시에 접근할 때 문제가 발생함
  • 동기화는 critical section에 접근한 경우 공유자원을 lock하여 다른 쓰레드가 접근하지 못하도록 구현함
  • 동기화를 잘못 구현하면 deadlock(교착상태)에 빠질 수 있음

 

자바에서의 동기화

synchronized 메서드

  • synchronized 메서드가 실행되는 동안 메서드가 포함된 객체에 lock을 검
  • 공유 변수를 변경하는 등 race condition이 발생할 수 있는 메서드에 사용
  • 자바에서는 deadlock을 방지하는 기술이 제공되지 않으므로 synchronized 메서드에서 다른 synchronized 메서드는 호출하지 않도록 해야함
public synchronized void deposit(int save) {

...
}

public synchronized void withdrawl(int minus) {

    ...
}

 

synchronized 메서드를 이용했을때 SyncMain.java 수행결과
  • 동기화를 구현하지 않았을 때와 달리 순차적으로 수행됨
  • 일단 하나의 쓰레드가 공유 자원에 접근하게 되면 다른 쓰레드는 끝날 때까지 기다림 (block; 대기 상태)
start deposit
start withdrawl
deposit(3000): 13000	// deposit 수행이 끝나면 withdrawl 수행
withdrawl(1000): 12000

 

synchronized 블럭

  • 매개 변수로 lock을 걸 객체를 입력함
  • 메서드 안에서 혹은 쓰레드에서 사용할 수 있음
  • 동기화가 이루어질 영역을 직접 설정할 수 있어 synchronized 메서드보다 디테일하게 설정할 수도 있음  
  • 메서드에서 사용하는 경우
public void deposit(int save) {

        synchronized(this) {	// 메서드에서 block 사용
            int m = getMoney();

            try {
                Thread.sleep(3000);	// 3초 소요
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            setMoney(m + save);
        }
}

 

  • 쓰레드에서 사용하는 경우
class Park extends Thread{
	
	public void run() {
		synchronized(SyncMain.myBank) {	// 쓰레드에서 block 사용
			System.out.println("start deposit");
			SyncMain.myBank.deposit(3000);
			System.out.println("deposit(3000): " + SyncMain.myBank.getMoney());
		}
	}
}