티스토리 뷰

이전에 포스팅한 join() 메서드에서도 언급했듯, Thread는 Main 메서드의 순서와는 별개로 실행된다는 것이 장점이자 단점이다. 

즉, 실행 속도에 따라 원치 않는 결과를 얻을 수도 있는 것이다.

 

간단한 예시로 한 은행 계좌에 두 명의 사람이 각각 입금과 출금을 한다고 가정하고 코드를 짜 보자.


○Bank.java

package com.choonham;

public class Bank {
	
	private int money = 10000;  //현재 잔액 초기 값
	
	public Bank() {
		// TODO Auto-generated constructor stub
	}

	
	/* 입금 처리 메서드 */
	public void MoneyIn(int save) {
		int m = this.money;
		try {
			Thread.sleep(3000); 
		}catch(InterruptedException e) {
			System.out.println(e.getMessage());
		}
		this.setMoney(m + save);
	}
	
	/* 출금 처리 메서드 */
	public void moneyOut(int needed) {
		int  m = this.money;
		try {
			Thread.sleep(200); 
		}catch(InterruptedException e) {
			System.out.println(e.getMessage());
		}
		this.setMoney(m - needed);
	}


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

○Me.java

package com.choonham;

public class Me extends Thread{

	public Me() {
		// TODO Auto-generated constructor stub
	}
	
	public void run() {
		MainClass.bnk.MoneyIn(3000);
		System.out.println("Money In " + MainClass.bnk.getMoney());
	}

}

○Wife.java

package com.choonham;

public class Wife extends Thread{

	public Wife() {
		// TODO Auto-generated constructor stub
	}
	
	public void run() {
		MainClass.bnk.moneyOut(1000);
		System.out.println("Money Out  : " + MainClass.bnk.getMoney());
	}

}

○Main.java

package com.choonham;

public class MainClass {
	
	public static Bank bnk = new Bank();
	
	public static void main(String[] args) {
		System.out.println("현 잔액 : " + bnk.getMoney());
		
		Me m = new Me();
		Wife w = new Wife();
		
		m.start();

		w.start();
		
		
		
	}

}

a위와 같이 멀티 스레드를 이용하여 Me 클래스(3000원 입금)와 Wife 클래스(1000원 출금)를 Main에서 실행시키게 되면, 정상적인 순서를 따른다면, 3000원 입금 후 1000원을 출금해야 하지만 두 스레드는 각각 따로 실행되어, 공유하는 account의 money변수를 거의 동시에 접근하게 된다.

결과 값을 보면, 

Output: 

이렇게 원치 않는 데이터를 얻게 되는 문제점이 있다.


그렇기 때문에 join() 메서드를 사용하여 이를 제어할 수 있지만, join() 메서드는 생성한 Thread 만큼 사용하여 각각 따로, 동기화시켜야 한다는 번거로움을 가지고 있다. (안 되는 건 아니다.) 

MainClass만 join()을 추가하여 결과를 확인해보면,

○Main.java:

package com.choonham;

public class MainClass {
	
	public static Bank bnk = new Bank();
	
	public static void main(String[] args) {
		System.out.println("현 잔액 : " + bnk.getMoney());
		
		Me m = new Me();
		Wife w = new Wife();
		
		m.start();
		try {
			m.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		w.start();
	}

}

Output:

데이터는 잘 나온다. 하지만 아무리 생각해봐도 Thread가 조금만 많아지면 굉장히 불편할 거 같다. 


그래서 사용하는 개념이 바로 run 메서드를 설계할 때부터 동기화시켜 순서를 따르게 하는 동기화(Synchronization)의 기법이다.

synchronized {}는 메서드와 블록 형태로 사용이 가능하다. 즉, synchronized로 묶을 경우, 블록 안에 모든 멤버 변수들은 Lock이 걸려, 순서에 맞춰 실행이 되게 되는 것이다. (뭐, 그럴 거면 왜 Thread를 사용하나 싶긴 하지만, 경우야 많은 거니까..)

동일한 예시로 synchronized를 사용해보면, 2가지 방법으로 동기화를 적용해볼 수 있다.


1. Bank 클래스의 모든 매서드에 적용시킨다. 

 

○Bank.java

package com.choonham;

public class Bank {
	
	private int money = 10000;  //현재 잔액 초기 값
	
	public Bank() {
		// TODO Auto-generated constructor stub
	}

	
	/* 입금 처리 메서드 */
	public  void MoneyIn(int save) {
		synchronized(this) {
		int m = this.money;
		try {
			Thread.sleep(3000); 
		}catch(InterruptedException e) {
			System.out.println(e.getMessage());
		}
		this.setMoney(m + save);
		}
	}
	
	/* 출금 처리 메서드 */
	public void moneyOut(int needed) {
		synchronized(this) {
		int  m = this.money;
		try {
			Thread.sleep(200); 
		}catch(InterruptedException e) {
			System.out.println(e.getMessage());
		}
		this.setMoney(m - needed);
		}
	}


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

이렇게 적용시키면, Main에서 join을 사용하는 것과 동일한 결과를 얻을 수 있다.

 


2. Thread를 상속받고 run을 사용하는 class의 run()에 각각 적용시킨다.

○Me.java

package com.choonham;

public class Me extends Thread{

	public Me() {
		// TODO Auto-generated constructor stub
	}
	
	public void run() {
		synchronized (this) {
		MainClass.bnk.MoneyIn(3000);
		System.out.println("Money In " + MainClass.bnk.getMoney());
		}
	}

}

○Wife.java

package com.choonham;

public class Wife extends Thread{

	public Wife() {
		// TODO Auto-generated constructor stub
	}
	
	public void run() {
		synchronized (this) {
		MainClass.bnk.moneyOut(1000);
		}
		System.out.println("Money Out  : " + MainClass.bnk.getMoney());
	}

}

마찬가지로 동일한 결과 값을 얻어낼 수 있다.


앞으로 프로젝트를 진행하면서 Thread를 포함한 개념들을 얼마나 많이 사용할지는 모르겠지만, 그게 중요한 건 아니니, 잘 알아두도록 하자. 

 

끝!

Comments