은행에서는 하나의 계좌에 입금과 출금을 할 수 있습니다. 여기서 계좌는 공유 자원에 해당하고, 입금과 출금 각각은 공유 자원에 접근하고자 하는 프로세스라고 볼 수 있습니다. 이를 자바로 구현한 코드는 다음과 같습니다.
public class ThreadSyncEx {
public static void main(String[] args) {
Runnable thread = new CreateThread();
// 2개의 작업 스레드 생성
Thread thread1 = new Thread(thread);
Thread thread2 = new Thread(thread);
thread1.setName("스레드1");
thread2.setName("스레드2");
thread1.start();
thread2.start();
}
}
class Money {
// 현재 가지고 있는 금액
private int myMoney = 10000;
public int getMyMoney() {
return myMoney;
}
public void withdraw(int money) {
// 가지고 있는 금액이 출금할 금액보다 크거나 같을 때만 출금
if (myMoney >= money) {
// 스레드 동시 접근을 위한 코드
try {
Thread.sleep(1000);
} catch (Exception e) {}
// 출금
myMoney -= money;
System.out.printf("스레드: %s , 출금액: %d원 남은금액: %d원 \n", Thread.currentThread().getName(), money, myMoney);
return;
}
System.out.printf("스레드: %s , 출금액: %d원 출금 거부 \n",Thread.currentThread().getName(),money);
}
}
class CreateThread implements Runnable {
Money myMoney = new Money();
public void run() {
while (myMoney.getMyMoney() > 0) {
// 1000 ~ 5000원씩 출금
int money = (int)(Math.random() * 5 + 1) * 1000;
// 출금 실행 코드
myMoney.withdraw(money);
}
}
}
실행 결과는 다음과 같습니다. 뭔가 이상하죠? 남은 금액도 이상하고 코드상으로는 남은금액이 마이너스가 될 수 없는데 말입니다.
그 이유는 당연히 코드 동기화가 되어 있지 않아서 입니다.
myMoney -= money
코드상에서 가장 문제가 있는 부분은 이 공유 자원의 값을 변경하는 부분입니다. 자바 코드상으로는 이 한 줄만 실행되는 것 같지만 로우 레벨로 내려가서 바이트코드를 보면 여러 줄로 구현이 되어 있기 때문입니다. 이를 해결하기 위해서는 공유 자원에 접근하는 쓰레드는 하나만 존재하도록 관리해야합니다.
은행계좌 문제에 세마포를 적용해보자. 위에서 임계구역은 Money 클래스 내부에서 myMoney의 값을 변경하는 부분이다. 따라서 임계 구역 앞뒤로 acquire와 release를 적용하면 다음과 같이 바꿀 수 있다.
Semaphore(int permits, boolean fair) : 공유 자원에 접근할 수 있는 스레드의 수를 permits 매개변수를 통해 지정할 수 있다. fair가 false면 순서가 보장되지 않는다.
acquire() throws InterruptedException
: semaphore 의 permit 을 하나 가져간다. ( -1 시킨다 )
permit 이 양수였을 경우에는 스레드가 계속 동작을 계속하고 그렇지 않으면 permit 을 획득할 때까지 혹은 interrupt 되기 전까지 waiting 한다.
release()
: permit을 release한다 ( +1 시킨다)
permit을 획득하려고 대기하는 스레드가 있으면 fair 정책에 따라 해당 스레드가 깨어난다.
class Money {
private int myMoney = 10000;
Semaphore semaphore;
public MoneyWithSemaphore() {
semaphore = new Semaphore(1); // value 값을 1로 초기화
}
public int getMyMoney() {
return myMoney;
}
public void withdraw(int money) {
try{
semaphore.acquire(); //임계 구역에 들어가기를 요청
}catch (InterruptedException e){}
try{
// 가지고 있는 금액이 출금할 금액보다 크거나 같을 때만 출금
if (myMoney >= money) {
// 스레드 동시 접근을 위한 코드
try {
Thread.sleep(1000);
} catch (Exception e) {}
// 출금
myMoney -= money;
System.out.printf("스레드: %s , 출금액: %d원 남은금액: %d원 \n", Thread.currentThread().getName(), money, myMoney);
return;
}
System.out.printf("스레드: %s , 출금액: %d원 출금 거부 \n",Thread.currentThread().getName(),money);
}finally {
semaphore.release();
}
}
}
정상적으로 동작하는 걸 확인할 수 있다.
세마포는 상포 배제(Mutual Exclusion)뿐만 아니라 프로세스간 순서를 제어하기 위해서도 사용됩니다. 예를 들어 프로세스 P1, P2 두 개가 있다고 가정하면, 원하는 순서는 P1, P2 순으로 실행하기를 원합니다. 그러면 아래와 같이 설정해주면 됩니다.
P1 | P2 |
---|---|
sem.acquire() | |
Section1 | Section2 |
sem.release() |
P1이 먼저 실행되는 경우에는 먼저 Section1에 들어간 후 실행이 끝나면 sem.release()
가 호출되면서 value 값이 1 증가됩니다.
그리고 나서 P2가 sem.acquire()
를 실행하면서 Section2에 진입할 수 있게 됩니다.
중요한 부분은 이 부분입니다.
sem.acquire()
을 호출하게 됩니다. sem.release()
를 호출하게 됩니다.따라서 다음과 같이 세마포를 작성하면 P1 - > P2 순서대로 실행시킬 수 있습니다.
이번에는 출금 스레드와 입금 스레드를 나누어서 생성하였습니다. 그리고 semaphoreOrder라는 순서 제어를 위한 세마포를 추가해주었습니다. 주의할 점은 semaphoreOrder는 처음에 value 값을 0으로 초기화 주었고, 입금이 항상 먼저 실행되게끔 하기 위해 withdraw메서드 안에 다음과 같이 코드를 작성해 주었습니다.
try{
semaphoreOrder.acquire(); // 순서제어를 위한 세마포
semaphore.acquire(); // 상호 배제를 위한 세마포
}catch (InterruptedException e){}
전체 코드는 다음과 같습니다.
import java.util.concurrent.Semaphore;
public class ThreadSyncEx3 {
public static void main(String[] args) {
MoneyWithSemaphoreWithOrder money = new MoneyWithSemaphoreWithOrder();
// 2개의 작업 스레드 생성
Thread withdrawThread = new Thread(new WithdrawThread(money));
Thread depositThread = new Thread(new DepositThread(money));
withdrawThread.setName("출금 스레드");
depositThread.setName("입금 스레드");
withdrawThread.start();
depositThread.start();
}
}
class MoneyWithSemaphoreWithOrder {
// 현재 가지고 있는 금액
private int myMoney = 10000;
Semaphore semaphore,semaphoreOrder;
public MoneyWithSemaphoreWithOrder() {
semaphore = new Semaphore(1); // value 값을 1로 초기화
semaphoreOrder = new Semaphore(0); // value 값을 0으로 초기화
}
public int getMyMoney() {
return myMoney;
}
public boolean withdraw(int money) {
try{
semaphoreOrder.acquire();
semaphore.acquire(); //임계 구역에 들어가기를 요청
}catch (InterruptedException e){}
try{
return doWithdraw(money); // 임계 구역
}finally {
semaphore.release(); //임계 구역에서 나가면서 lock을 release한다.
}
}
private boolean doWithdraw(int money) {
if (myMoney >= money) {
try {
Thread.sleep(10);
} catch (Exception e) {}
myMoney -= money;
System.out.printf("스레드: %s , 출금액: %d원 남은금액: %d원 \n",Thread.currentThread().getName(), money, myMoney);
return true;
}
System.out.printf("스레드: %s , 출금액: %d원 출금 거부 \n",Thread.currentThread().getName(),money);
return false;
}
public void deposit(int money) {
try{
semaphore.acquire(); //임계 구역에 들어가기를 요청
}catch (InterruptedException e){}
try{
doDeposit(money); // 임계 구역
}finally {
semaphore.release(); //임계 구역에서 나가면서 lock을 release한다.
semaphoreOrder.release();
}
}
private void doDeposit(int money) {
try {
Thread.sleep(10);
} catch (Exception e) {}
myMoney += money;
System.out.printf("스레드: %s , 입금액: %d원 남은금액: %d원 \n", Thread.currentThread().getName(), money, myMoney);
}
}
class DepositThread implements Runnable {
private MoneyWithSemaphoreWithOrder myMoney;
DepositThread(MoneyWithSemaphoreWithOrder myMoney) {
this.myMoney = myMoney;
}
@Override
public void run() {
while (true){
int money = (int)(Math.random() * 5 + 1) * 1000;
myMoney.deposit(money);
}
}
}
class WithdrawThread implements Runnable {
private MoneyWithSemaphoreWithOrder myMoney;
WithdrawThread(MoneyWithSemaphoreWithOrder myMoney) {
this.myMoney = myMoney;
}
@Override
public void run() {
while (true) {
// 1000 ~ 5000원씩 출금
int money = (int)(Math.random() * 10 + 1) * 1000;
// 출금 실행 코드. 실패시 true 반환
boolean denied = !myMoney.withdraw(money);
}
}
}
자바에서는 synchronized
키워드를 이용해 모니터를 이용한 동기화를 지원하고 있습니다.
synchronized
를 사용하는 방법으로는 메서드 앞에 키워드 명시, 인스턴스로 사용하기가 있다. 동기화가 필요한 메서드 앞에 synchronized
키워드만 붙여주면 편리하게 동기화를 적용할 수 있다.
synchronized
(메서드) { 구현 } 으로 사용할 수 있다.class MoneyWithMonitor {
// 현재 가지고 있는 금액
private int myMoney = 10000;
public int getMyMoney() {
return myMoney;
}
public synchronized boolean withdraw(int money) {
try{
return doWithdraw(money); // 임계 구역
}finally {
}
}
private boolean doWithdraw(int money) {
if (myMoney >= money){
try {
Thread.sleep(10);
} catch (Exception e) {}
myMoney -= money;
System.out.printf("스레드: %s , 출금액: %d원 남은금액: %d원 \n",Thread.currentThread().getName(), money, myMoney);
return true;
}
System.out.printf("스레드: %s , 출금액: %d원 출금 거부 \n",Thread.currentThread().getName(),money);
return false;
}
public synchronized void deposit(int money) {
try{
doDeposit(money); // 임계 구역
}finally {
}
}
private void doDeposit(int money) {
try {
Thread.sleep(10);
} catch (Exception e) {}
myMoney += money;
System.out.printf("스레드: %s , 입금액: %d원 남은금액: %d원 \n", Thread.currentThread().getName(), money, myMoney);
}
}
실행결과를 확인해보겠습니다. 동기화가 잘 이루어진 걸 확인할 수 있습니다.