[Java] 멀티 스레드 동시성 제어
하이 ..!
지난번 글에서 내가 멀티 스레드에 대해 포슽팅을 했었다.
https://devkingdom.tistory.com/275
오늘은 Multi Thread 의 동시성을 처리하는 방법에 대해 좀 얘기 해볼까한다.
블로그명이 IT 알려주는 은행원 이니 입금, 출금 등 은행 거래를 예시로 설명을 드리려 한다.
위의 그림에 표현된 계좌에는 동시 다발적으로 입금, 출금 ,조회가 일어난다. 그런데 여기서 문제가 하나 보인다. 바로 저 거래1과 2라는 스레드가 동일한 잔액이라는 데이터를 건드린다는 것이다.
예를들어서 각 거래가 동시에 잔액을 변경한다고 해보자. 거래 1은 2000원을 출금하는것이고 거래 2는 4000원을 입금하는 거라면 잔액은 12000이 되어야 한다. 이렇게 단순하게 2건만 수행된다면 잘 수행될 것이다. 하지만 이러한 거래가 연속적으로 10000건이 일어난다고 가정해보자. 과연 이 거래는 안전하게 수행이 될까?
아래 코드를 보자.
package account;
public class Account {
static long balance = 10000;
public void deposit(long amount) {
balance = balance + amount;
}
public void withdraw(long amount) {
balance = balance - amount;
}
public void inquiry() {
System.out.println("현재 잔액은 " + balance + "원 입니다");
}
}
먼저 해당 클래스는 계좌 클래스로 잔액이 10000원이 있고 static 이다. static으로 해뒀기에 스레드가 해당 변수를 공유할 수 있따. 그리고 입금, 출금 , 조회 메서드가 있다.
package account;
public class DepositThread implements Runnable{
Account account;
DepositThread(Account account) {
this.account = account;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
account.deposit(1000);
}
account.inquiry();
}
}
위의 클래스는 계좌에 입금을 하는 스레드다. 해당 스레드는 1000원씩 1000번을 입금한 후 계좌를 조회한다.
package account;
public class WithdrawThread implements Runnable{
Account account;
WithdrawThread(Account account) {
this.account = account;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
account.withdraw(1000);
}
account.inquiry();
}
}
위의 클래스는 계좌에 출금을 하는 스레드다. 해당 스레드는 1000원씩 1000번을 출금한 후 계좌를 조회한다.
package account;
public class Bank extends Thread {
public static void main(String[] args) {
Account account = new Account();
for(int i=0; i<10; i++) {
Thread deposit = new Thread(new DepositThread(account));
Thread withdraw = new Thread(new WithdrawThread(account));
deposit.start();
withdraw.start();
}
}
}
마지막으로 위의 코드는 각 출금과 입금 스레드를 10번씩 수행 시킨다.
각각 출금과 입금 횟수가 동일하기에 잔액이 10000이 나와야 정상일 것이다.
한번 돌려보자.
결과가 이상하게 나온다 ... 이렇게 스레드가 돌아갈때 동기화 처리를 안해주면 이런 무서운 결과를 만들게된다.
여기서 우리는 스레드가 자원에 동시 접근하는 것을 제어해줘야하는데 이를 상호베제 , Mutex(Mutual Exclusion)이라고 한다. 그리고 동기화작업을 해준 클래스는 Thread safe 하다고 표현하게 된다.
이제 클래스를 Thread safe 하게 만들어보자.
package account;
public class Account {
static long balance = 10000;
public void deposit(long amount) {
balance = balance + amount;
}
public void withdraw(long amount) {
balance = balance - amount;
}
public void inquiry() {
System.out.println("현재 잔액은 " + balance + "원 입니다");
}
}
Synchronized 키워드
아래 수정된 Account 클래스를 보자.
package account;
public class Account {
static long balance = 10000;
public synchronized void deposit(long amount) {
balance = balance + amount;
}
public synchronized void withdraw(long amount) {
balance = balance - amount;
}
public void inquiry() {
System.out.println("현재 잔액은 " + balance + "원 입니다");
}
}
보시면 deposit 메서드와 withdraw 메서드에 synchronized 키워를 붙였다.
그리고 다시 수행해보도록하자.
이번에는 10000원이 딱 나오는 것을 볼 수 있다.
synchronized 블럭
동기화는 블럭단위로도 가능하다. 예를 들면 은행에서 공과금을 처리하거나 카드대금을 결제하는 등의 배치성 업무는 몇십만건이 수행된다. 이럴때 그런 로직은 트랜잭션을 유지해줘야한다. 이렇게 블럭단위로도 동시성 제어가 가능하다.
아래 클래스를 보자.
package account;
public class DepositThread implements Runnable{
Account account;
DepositThread(Account account) {
this.account = account;
}
@Override
public void run() {
synchronized (account) {
for (int i = 0; i < 1000; i++) {
account.deposit(1000);
}
account.inquiry();
}
}
}
package account;
public class WithdrawThread implements Runnable{
Account account;
WithdrawThread(Account account) {
this.account = account;
}
@Override
public void run() {
synchronized (account) {
for (int i = 0; i < 1000; i++) {
account.withdraw(1000);
}
account.inquiry();
}
}
}
보면 입금,출금 둘다 1000건씩 돌아가고 조회하는 로직을 synchronized 블럭을 묶어놨다.
이렇게하면 1000 x 1000 즉 1000000원 씩 거래 단위가 움직일 것이다.
이런식으로 말이다.
오늘은 여기까지 정리하도록 하겠다.
잘 정리하여 앞으로 동시성을 제어하는데 문제를 겪지 않길 바란다...