쓰레드의 동기화

김민성·2022년 7월 14일
0

Java

목록 보기
36/47
post-thumbnail

쓰레드의 동기화

멀티쓰레드 프로세스는 여러 쓰레드가 같은 자원을 공유해서 작업하므로 서로의 작업에 영향을 주게 된다. 이러한 현상을 방지하기 위해서 임계 영역잠금 (lock) 같은 개념을 도입해 간섭을 막는데, 이것을 쓰레드의 동기화 라고 한다.

public class Main{
    public static void main(String[] args) {
        Runnable r = new RunnableImpl();
        new Thread(r).start();
        new Thread(r).start();
    }
}

class Account {
    private int balance = 1000;

    public int getBalance(){
        return balance;
    }

    public void withdraw(int money){
        if(balance >= money){
            try {
                // sleep : '잔고가 음수' 라는 상황을 연출하기 위해 다른 쓰레드에게 제어권을 넘겨줌
                Thread.sleep(1000);
            }catch (InterruptedException e){}
            balance -= money;
        }
    }
}

class RunnableImpl implements Runnable{
    Account acc = new Account();

    @Override
    public void run() {
        while(acc.getBalance() > 0){
            // 100, 200, 300 중 랜덤한 값을 출금
            int money = (int)(Math.random() * 3 + 1) * 100;
            acc.withdraw(money);
            System.out.println("잔고 : " + acc.getBalance());
        }
    }
}

/*
실행 결과
잔고 : 900
잔고 : 900
잔고 : 800
잔고 : 500
잔고 : 100
잔고 : 100
잔고 : 300
잔고 : 0
잔고 : -100
*/

위의 코드에서 withdraw 메서드에서 분명 balance >= money 인 조건에서만 출금하도록 설정했는데, 몇번 실행하다 보면 잔고가 음수가 나올 때가 있다.

이는 한 쓰레드가 if문의 조건식을 통과하고 출금하기 바로 직전에 다른 쓰레드가 끼어들어 먼저 출금했기 때문에 발생한 문제이다.

synchronized를 이용한 동기화

synchronized 키워드는 임계영역을 설정하는데 사용한다.

임계영역 (critical section) : 쓰레드들이 공유하는 공유 데이터를 사용하는 코드에 대해 영역을 지정해 특정 자격을 가진 쓰레드만이 이 영역의 코드를 수행할 수 있게 설정함.

이 떄 특정 자격이란 공유 데이터가 가지고 있는 lock을 획득한 쓰레드를 의미하며, lock을 가진 쓰레드는 임계영역의 코드를 수행한 후 lock을 반납한다.

public synchronized void section1(){
    // 내부는 임계영역
}

synchronized(객체의 참조변수){
    // 내부는 임계영역
}

위의 코드와 같이 임계영역을 설정하는 방법은 두 가지가 있다.

  1. 메서드 전체를 임계영역으로 지정

    메서드 앞에 synchronized 키워드를 붙인다.

    쓰레드는 이 메서드가 호출된 시점부터 해당 메서드가 포함된 객체의 lock을 얻는다.

  2. 특정 영역을 임계영역으로 지정

    이 때의 참조변수는 lock을 걸고자 하는 객체를 참조하는 것이어야 한다.

    쓰레드는 이 synchronized블럭 영역 안으로 들어가면서 객체의 lock을 얻는다.

임계영역은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에 임계영역을 최소화하는 synchronized 블럭을 사용하는게 효율적이다.

동기화를 이용해 위의 잔고 예제의 오류를 고쳐보자.

동기화 이슈가 발생하는 if문 메서드쪽에 synchronized 키워드를 사용하면 된다.

// 메서드 전체를 임계영역으로 지정
public synchronized void withdraw(int money){
    if(balance >= money){
        try {
            Thread.sleep(1000);
        }catch (InterruptedException e){}
        balance -= money;
    }
}

// 특정 영역을 임계영역으로 지정
public synchronized void withdraw(int money){
    synchronized(this){
        if(balance >= money){
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e){}
            balance -= money;
        }
    }
}

wait(), notify() - 유연한 동기화의 처리

동기화를 해서 공유 데이터를 보호하는 것도 물론 중요하지만, 특정 쓰레드가 객체의 lock을 오래 가지고 있지 않도록 하는 것도 중요하다. 예를 들어 계좌의 돈이 부족한 상황에서 한 쓰레드가 lock을 가진 채 돈이 입금될 때 까지 기다린다면, 순서가 밀려 원활한 진행이 어려울 것이다.

이 때 wait()notify()를 이용해 lock의 획득과 반납을 적절하게 조절하는 것이다.

  • wait() : 임계 영역의 코드를 수행하다가 더 이상 진행할 상황이 아니면 호출해 lock을 반납하고 기다리게 한다.
  • notify() : 다시 진행가능한 상황이 오면 호출해 작업을 중단했던 쓰레드가 다시 lock을 얻어 작업을 수행한다.

두 메서드 모두 동기화 블록 내에서만 사용할 수 있다.

notifyAll() : 기다리고 있는 모든 쓰레드에게 통보하지만, 결국 lock을 얻는 쓰레드는 단 하나이다.

이 메서드가 호출된다고 해서 모든 객체의 waiting pool에 있는 쓰레드가 깨워지는 것이 아닌, 호출된 객체의 waiting pool에 대기중인 쓰레드만 깨워진다는 사실을 기억하자. (waiting pool은 객체마다 존재하는 쓰레드 대기실 이라고 생각하면 된다.)

0개의 댓글