JAVA/JAVA Basic

[ JAVA ] 스레드(Thread) 동기화3 ( 모니터(Monitor) )

IT록흐 2021. 6. 20. 09:12
반응형
 

[ JAVA ] 스레드(Thread) 동기화2 (세마 포어 Semaphore)

이전 포스팅에서는 공유 객체를 향한 스레드의 경쟁이 적은 경우의 상황을 알아 보았다. 이번에는 공유객체를 향해 스레드의 경쟁이 심해지는 경우, 어떤 방식으로 JVM이 스레드 동기화를 시도

lordofkangs.tistory.com

 

이전 포스팅에서

동기화를 위한 상호배제 방법으로

'세마포어(Semaphore)에 대해서 알아 보았다.

 

하지만 세마포어는 JVM이

채택하고 있는 방식이 아니다.

JVM은 상호배제를 위해

Monitor 방식을 사용한다.

 

 

[ JAVA ] 스레드(Thread) 동기화1 ( Intrinsic Lock + 피터슨 알고리즘 )

동기화 지훈이네 가족은 아빠, 엄마 그리고 형까지 네 식구로 이루어져 있다. 그러나 집 안에 컴퓨터가 단 한 대 뿐이라 가족간의 충돌이 불가피했다. 그래서 지훈이네 가족은 컴퓨터를 쓰는 순

lordofkangs.tistory.com

 

 

'스레드 동기화 이해하기 1' 포스팅에서 경쟁의 정도에 따라 객체의 상호배제 방식이 바뀐다고 말했다. 객체의 헤더에 있는 Mark Word가 경쟁이 없을 때는 Biased Lock이 되고 경쟁 스레드가 2개가 되면 Light Weight Lock이 된다. 그리고 공유 객체를 두고 경쟁 스레드가 3개 이상으로 넘어가면 Heavy Weight Lock 상태가 되는데, 이때 실질적으로 Monitor 방식의 상호배제가 사용된다.

 

 

Mark Word에 대한 구체적인 내용은 위 포스팅을 참고하기를 바란다. Heavy Weight Lock 상태가 되면, Mark Word는 아래와 같이 바뀐다.

 

Heavy Weight Lock 상태의 Mark Word

 

 

공유객체에 접근한 스레드는 Mark Word를 통해, 현재 Monitor 방식의 상호배제가 이루어지고 있음을 확인하다. 그 후, Monitor Address를 통해, 모니터에 접근한다.

 

Monitor의 구조는 아래와 같다.

 

 

 

 

 

 

 

 

 

Mark Word의 Monitor Address를 통해, Monitor에 접근한 스레드는 Entry Set에 대기한다. Monitor에 접근 가능한 스레드는 단 한 개뿐이다. Monitor는 크게 두 가지로 구성된다. 1. 공유 자원 2. 프로시저 (wait() 와 notify()는 다음 포스팅에서 자세히 다룰 예정이다.)

 

일단 자바 코드를 하나 보겠다.

public class Account { // 공유 객체

	private int account = 30000; // 공유 자원
	
   // 동기화 메소드 (프로시저1)
	public synchronized void withDraw(int money) {
		 account -= money; // 공유 자원에 접근
	}
   // 동기화 메소드 (프로시저2)
   public synchronized void deposit(int money) {
		 account += money; // 공유 자원에 접근
	}
}

 

JAVA에서 synchronized 키워드가 사용된 객체는 공유 객체가 된다. JVM은 해당 객체를 토대로 Monitor를 만든다. '공유 자원'은 동기화된 (synchronized) 메소드가 접근하는 공유 객체의 필드가 된다. 그리고 동기화된 메소드들은 '프로시저'가 된다. 스레드가 모니터에 접근하면, 스레드는 공유자원에 직접 접근할 수 없다. 무조건 프로시저를 통해 접근해야 한다.

 

 

스레드 하나가 프로시저 withDraw()에 접근하면, withDraw()는 스레드 대신 account에 접근한다. Monitor에는 한 스레드만 접근 가능하므로, 스레드가 deposit() 프로시저를 사용하지 않아도 다른 스레드는 deposit() 프로시저에 접근할 수 없다.

 

이처럼 JVM은 Monitor를 ADT(추상 데이터 타입)으로 정의해놓고, Monitor가 필요한 공유 객체를 위해 ADT(일종의 설계도)를 토대로 해당 공유객체 전용 모니터를 만든다.

 

이와 같이 자바에서는 간단하게 동기화 작업을 할 수 있다. 세마포어의 경우, 세마포어 객체를 생성해주고, acquire()와 release() 메소드를 임계영역 전후로 동기화 메소드마다 위치시켜야 하는 수고가 있다.

 

하지만 JAVA에서는 동기화가 필요한 영역에 synchonized 키워드만 넣어주면 그 뒷일은 알아서 JVM이 처리해준다. 그러므로 개발자가 코드상에 실수할 우려가 줄어드니 동기화 효율은 증가한다. 그럼 코드를 통해서, 구체적으로 알아보자.

 

해당 포스팅을 참고하여 만들어 보았습니다.

 

 

java - synchronized 란? 사용법?

java - synchronized 란? 사용법? 멀티스레드를 잘 사용하면 프로그램적으로 좋은 성능을 낼 수 있지만, 멀티스레드 환경에서 반드시 고려해야할 점인 스레드간 동기화라는 문제는 꼭 해결해야합니다

coding-start.tistory.com

 

우선 synchronized를 사용하지 않으면 어떤 결과가 출력되는지 확인해보겠다.

 

< 공유 객체 클래스 >

package synchronizedthread;

public class BalanceSystem { // 잔액 관리 공유 클래스

	private int balance = 30000; // 공유 자원
	
	public int getBalance() {
		return balance;
	}
    
    //synchronized 안 됨
	public boolean systemActivated(int withDraw) { 

	// 출금액이 잔액보다 큰 경우
      if(balance < withDraw) { 
			System.out.println("잔고가 부족하여 출금할 수 없습니다.");
			return false;
		}		
    // 출금액이 잔액보다 작은 경우
		System.out.println(Thread.currentThread().getName() + "의 출금 : " + withDraw +"원");
		
        //스레드 2초간 쉬기
		try {
			Thread.sleep(2000);
		}catch(Exception e) {}
		
		balance -= withDraw; // 출금 
		System.out.println("현재 잔액 : " + balance); // 현재 잔액 출력
		return true;
	}
}

 

 

systemActivated 메소드에 한 개의 스레드가 접근할 것이다. 하지만 접근을 했더라도 스레드는 출금을 하기 전 , 2초간 대기상태가 되어야한다. 2초간 멈춰있는 사이에 다른 스레드도 systemActivated 메소드에 접근한다. 문제는 아직 출금이 되지 않았다는 것이다. 그러므로 다른 스레드는 출금 처리 되기전의 공유자원(balance)을 사용하게 된다.이와 같은 경우를 두고 데이터의 무결성이 훼손되었다고 말한다.

 

<스레드 클래스>

package synchronizedthread;

public class User extends Thread{
	static int userCount=0; //스레드 이름짓기용 count
	private int withDraw; // 출금액
	private BalanceSystem bs; // 잔액 관리 시스템 참조변수
	
	public User() {
		setName("Thread"+userCount++); // 스레드 이름짓기
	}
	
	public void setWithDraw() {
		this.withDraw = ((int)(Math.random()*5+1))*1000;
         // 1000, 2000, 3000, 4000, 5000 중 하나 랜덤하게 선택
	}
	
	public void setBalanceSystem(BalanceSystem bs) {
        // 잔액 관리 시스템과 연결(공유 객체)
		this.bs = bs;
	}
	
	@Override
	public void run() {
		while(true) {
            // 출금액 선정
			setWithDraw(); 
            // 메소드 return 값이 fasle면 반복문 탈출
			if(!bs.systemActivated(withDraw)) break; 
	    }
     }
}

<Main 클래스>

package synchronizedthread;

public class Main {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		BalanceSystem  bs = new BalanceSystem(); // 잔액 관리 시스템 생성
		
		User user0 = new User(); // 스레드 생성
		user0.setBalanceSystem(bs); // 공유 객체 연결
		user0.start(); // 스레스 시작
		
		User user1 = new User(); // 스레드 생성
		user1.setBalanceSystem(bs);// 공유 객체 연결
		user1.start(); // 스레드 시작
	}
}

 

<출력 결과>

 

 

출금 후 현재 잔액을 표시하기도 전에, Thread0과 Thread1이 동시에 접근했음을 알수 있다.

 

그 후 마지막 현재 잔액을 보면 -5000원이 찍혔다. 메소드에 접근했을 때는, 잔액이 0보다 컸지만, 스레드 대기가 풀린 다른 스레드가 출금을 시도하면서 잔액은 0보다 같거나 작아진 것이다. 그 후 해당 스레드가 출금을 하니 잔액은 음수가 나온 것이다.

 

그럼 synchronized 키워드를 사용해보자.

public synchronized boolean systemActivated(int withDraw) {
		
		if(balance < withDraw) {
			System.out.println("잔고가 부족하여 출금할 수 없습니다.");
			return false;
		}
		
		System.out.println(Thread.currentThread().getName() + "의 출금 : " + withDraw +"원");
		
		try {
			Thread.sleep(1000);
		}catch(Exception e) {}
		
		balance -= withDraw;
		System.out.println("현재 잔액 : " + balance);
		return true;
	}
​

 

문제가 되는 메소드에 synchronized 키워드를 넣어주었다. 그럼 이제 실행해보자.

 

<출력결과>

 

 

스레드가 중구난방으로 메소드를 실행하지 않고 하나씩 하나씩 실행되는 것을 확인할 수 있다. 그리고 잔액이 조건에 맞게 음수가 나오지 않는다. 이처럼 synchronized 키워드를 활용하면 데이터의 훼손을 막을 수 있다.

 

 


정리

 

 

1. JAVA는 Heavy Weight Lock 방식 Monitor 상호배제를 사용한다.

2. synchronized된 메소드가 있는 공유 객체를 위해 JVM은 Monitor ADT를 토대로 전용 Monitor를 생성한다.

3. Monitor는 한번에 하나의 스레드만 접근가능하고, 공유자원의 접근은 프로시저가 대신 접근한다.

 

 


참고자료

 

https://happy-coding-day.tistory.com/8

https://www.programmersought.com/article/407747922/

https://examples.javacodegeeks.com/adt-java-tutorial/

https://www.youtube.com/watch?v=yWprp019_n4&list=UUOcPzXDWSrnaKXse9XOPiog

 

 

반응형