이전 포스팅에서 스레드에 대한 기초 개념을 알아보았다. 여기서 멀티스레드로 구현을 하다보면, 동기화는 때에 따라 필수적이다. 여러 스레드가 같은 프로세스 내의 자원을 공유하면서 작업할 때 서로의 작업이 다른 작업에 영향을 주기 때문에 이와같은 상황엔 동기화가 필수적이다.
스레드의 동기화를 위해선, 임계영역과 잠금(lock)을 활용한다.
임계영역을 지정하고, 임계영역을 가지고 있는 lock을 단 하나의 스레드에게만 빌려주는 개념이다. 따라서 임계구역 안에서 수행할 코드가 완료되면, lock을 반납해야한다.
임계영역 : 공유 자원에 단 하나의 스레드만 접근하도록(하나의 프로세스에 속한 스레드만 가능)
뮤텍스 : 공유 자원에 단 하나의 스레드만 접근하도록(서로 다른 프로세스에 속한 스레드도 가능)
이벤트 : 특정한 사건 발생을 다른 스레드에게 알림
세마포어 : 한정된 개수의 자원을 여러 스레드가 사용하려고 할 때 접근 제한
대기 가능 타이머 : 특정 시간이 되면 대기 중이던 스레드 깨움
아래의 코드는 의도적으로 계좌에 동시 접근이 가능하도록 만든 코드이다. 출금할 금액이 계좌 잔액보다 크면, 출금을 못하도록 설정하였으나, 실행 과정에서 의도적으로 스레드가 동시에 접근할 수 있도록 하였다.
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 boolean withdraw(int money) {
// 가지고 있는 금액이 출금할 금액보다 크거나 같을 때만 출금
if (myMoney >= money) {
// 스레드 동시 접근을 위한 코드
try {
Thread.sleep(1000);
} catch (Exception e) {
System.out.println(e);
}
// 출금
myMoney -= money;
return true;
}
return false;
}
}
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;
// 출금 실행 코드. 실패시 true 반환
boolean denied = !myMoney.withdraw(money);
// 출금 과정 출력
if (denied) {
System.out.println("출금 거부");
} else {
System.out.printf("스레드: %s 출금: %d원 남은금액: %d원\\n",
Thread.currentThread().getName(), money, myMoney.getMyMoney());
}
}
}
}
// 출력 (실행할 때마다 결과는 달라진다.)
스레드: 스레드1 출금: 5000원 남은금액: 5000원
스레드: 스레드2 출금: 2000원 남은금액: 5000원
스레드: 스레드2 출금: 1000원 남은금액: 4000원
출금 거부
스레드: 스레드1 출금: 1000원 남은금액: 3000원
스레드: 스레드2 출금: 2000원 남은금액: -2000원
스레드: 스레드1 출금: 3000원 남은금액: -2000원
결과를 보면 스레드가 출금을 동시에 실시하여 남은 금액이 3000원임에도 불구하고, 2000원 출금과 3000원 출금이 동시에 발생하여 -2000원되는 상황이 발생했다.
스레드 동기화를 하지 않으면 실제 서비스나 프로그램을 이용할 때, 이러한 상황이 실제로 일어날 수 있다.
임계 영역은 둘 이상의 스레드가 동시에 접근해서는 안되는 코드 영역이다. 즉, 하나의 스레드만이 코드를 실행할 수 있는 영역이다. lock은 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한을 의미한다.
🔎 동작과정
synchronized 키워드를 임계 영역으로 지정할 메서드의 반환 타입 앞에 입력하여 메서드 전체를 임계 영역으로 설정할 수 있다.
설정한 메서드가 호출되었을 때, 메서드를 실행할 스레드는 메서드가 포함된 객체의 락(Lock)을 얻으며, 다시 락(Lock)을 반납하기 전까지는 다른 스레드는 해당 메서드를 실행하지 못한다.
class Money {
private int myMoney = 10000;
public int getMyMoney() {
return myMoney;
}
// 메서드 전체를 임계영역으로 설정
public synchronized boolean withdraw(int money) {
if (myMoney >= money) {
try {
Thread.sleep(1000);
} catch (Exception e) {
System.out.println(e);
}
myMoney -= money;
return true;
}
return false;
}
}
임계영역으로 지정할 코드 상단에 synchronized 키워드를 쓰고 소괄호 () 안에 해당 영역이 포함된 객채의 참조를 입력하여 지정할 코드까지 중괄호{}로 묶으면 해당 영역으로 설정된다.
class Money {
private int myMoney = 10000;
public int getMyMoney() {
return myMoney;
}
public boolean withdraw(int money) {
// 메서드 전체를 임계영역으로 설정
synchronized (this) {
if (myMoney >= money) {
try {
Thread.sleep(1000);
} catch (Exception e) {
System.out.println(e);
}
myMoney -= money;
return true;
}
return false;
}
}
}
// 출력
스레드: 스레드1 출금: 1000원 남은금액: 9000원
스레드: 스레드2 출금: 2000원 남은금액: 7000원
스레드: 스레드1 출금: 4000원 남은금액: 3000원
출금 거부
출금 거부
스레드: 스레드1 출금: 3000원 남은금액: 0원
잔액이 음수가 발생할 경우가 생기더라도 스레드 동기화로 인해 출금이 거부된다!!