이전 포스팅에서
동기화를 위한 상호배제 방법으로
'세마포어(Semaphore)에 대해서 알아 보았다.
하지만 세마포어는 JVM이
채택하고 있는 방식이 아니다.
JVM은 상호배제를 위해
Monitor 방식을 사용한다.
'스레드 동기화 이해하기 1' 포스팅에서 경쟁의 정도에 따라 객체의 상호배제 방식이 바뀐다고 말했다. 객체의 헤더에 있는 Mark Word가 경쟁이 없을 때는 Biased Lock이 되고 경쟁 스레드가 2개가 되면 Light Weight Lock이 된다. 그리고 공유 객체를 두고 경쟁 스레드가 3개 이상으로 넘어가면 Heavy Weight Lock 상태가 되는데, 이때 실질적으로 Monitor 방식의 상호배제가 사용된다.
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이 처리해준다. 그러므로 개발자가 코드상에 실수할 우려가 줄어드니 동기화 효율은 증가한다. 그럼 코드를 통해서, 구체적으로 알아보자.
해당 포스팅을 참고하여 만들어 보았습니다.
우선 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
'JAVA > JAVA Basic' 카테고리의 다른 글
[ JAVA ] 스레드(Thread) 동기화5 (Implicit Lock vs Explicit Lock ) (0) | 2021.06.20 |
---|---|
[ JAVA ] 스레드(Thread) 동기화4 ( yield(), join() ) (0) | 2021.06.20 |
[ JAVA ] 스레드(Thread) 동기화2 (세마 포어 Semaphore) (2) | 2021.06.20 |
[ JAVA ] 스레드(Thread) 동기화1 ( Intrinsic Lock + 피터슨 알고리즘 ) (2) | 2021.06.20 |
[ JAVA ] 스레드(Thread) 우선순위 설정 (1) | 2021.06.20 |