본문 바로가기

Java/기초

[Java] 쓰레드(Thread) - 2/2 (동기화)

동기화(synchronization)

  • 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것이다.
  • 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않게 해주는 개념이 임계 영역(critical section, synchronized 구간), 잠금(락, lock) 이다.
    • 자바의 모든 객체(인스턴스, 클래스)는 락을 가지고 있다. synchronized 블록은 객체 단위로 락을 다룬다.
      • Lock : 공유 자원에 한번에 한 쓰레드만 read, write를 수행 가능하도록 한다.
  • 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 '쓰레드의 동기화(synchronization)'이라고 한다.
  • 쓰레드 실행(main) Class
public class ThreadEx121 {
    public static void main(String[] args) {
        RunnableEx21 runnableEx21 = new RunnableEx21();
        new Thread(runnableEx21).start();
        new Thread(runnableEx21).start();
    }
}
  • Runnable
public class RunnableEx21 implements Runnable{
    Account account = new Account();

    @Override
    public void run() {
        while (account.getBalance() > 0){
            int money = (int)(Math.random() * 3 + 1) * 100;
            account.withdraw(money);
            System.out.println("balance : " + account.getBalance());
        }
    }
}
  • Account(동기화 작업)
public class Account {
    private int balance = 1000;

    public int getBalance(){
        return this.balance;
    }

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

 

1. wait, notify

  • wait()
    • 실행 중이던 쓰레드는 해당 객체의 대기실(waiting pool)에서 통지 대기
  • notify()
    • 호출되면, 해당 객체의 대기실에 있던 모든 쓰레드 중에서 임의의 쓰레드만 통지
  • notifyAll()
    • 기다리고 있는 모든 쓰레드에게 통보
    • lock을 얻을 수 있는 쓰레드는 하나
    • 나머지는 lock대기 상태
  • 기아(starvation) 현상
    • notify()를 활용하면 위에서 말했듯이 임의의 쓰레드만 깨어나게 되며, 어쩌면 최악의 상황에서 원하는 쓰레드가 끝까지 깨어나지 못하는 상태가 올수도 있다.
    • 이러한 상황을 기아 현상이라고 한다. 이 현상을 막으려면 notify() 대신 notifyAll()을 활용해야 한다. 하지만 notifyAll()도 분명히 문제가 존재한다.
  • 경쟁 상태(race condition)
    • notifyAll()을 활용하면 모든 쓰레드를 깨울 수 있지만, 결국 객체당 락은 하나이기 때문에 락 하나를 두고 모든 쓰레드가 서로 경쟁하게 된다.
    • 이러한 것을 경쟁 상태라고 한다. 이 경쟁 상태를 개선하기 위해서는 결국에 자신이 원하는 쓰레드를 직접 지명하여 통지해야 한다.

2. Lock, Condition

1) Lock

동기화할 수 있는 방법은 synchronized블럭 외에도 'java.util.concurrent.locks'패키지가 제공하는 locks클래스들을 이용하는 방법이 있다. synchronized블럭을 사용하게 되면 자동으로 lock이 걸리고 풀리기 때문에 편하다. synchronized블럭 영역에서 예외가 발생해도 lock은 자동으로 풀리다.

그러나 같은 메서드 내에서만 lock을 걸 수 있다. 이 제약이 문제될 때 lock클래스를 사용한다.

  • ReentracntLock
    • 가장 일반적인 Lock
    • 특정 조건에서 Lock을 플고, 나중에 다시 Lock을 얻어서 Critical Section으로 들어와서 이후의 작업을 수행할 수 있다
  • ReentrantReadWriteLock
    • 읽기를 위한 Lock과 쓰기를 위한 Lock을 따로 제공
    • 읽기 Lock이 걸려있으면, 다른 쓰레드가 읽기 Lock을 중복해서 걸고 읽기를 수행할 수 있다.
    • 읽기 Lock이 걸린 상태에서 쓰기 Lock을 거는 것은 허용하지 않는다. 또한 반대의 경우도 또한 마찬가지이다.
  • StampedLock
    • Lock을 걸거나 해지할 때 '스탬프(long타입의 정수값)'을 사용하며, 읽기와 쓰기를 위한 lock외에 '낙관적 읽기 lock'이 추가된 것이다.
    • 읽기 lock이 걸려있다면 쓰기 lock을 걸기 위해서는 읽기 lock이 풀릴 때 까지 기다려야하는데에 비해 '낙관적 읽기 lock'은 읽기 lock이 걸려있는 상태에서 쓰기 lock을 건다면, 읽기 lock은 바로 풀리게 된다.
    • 무조건 읽기 lock을 걸지 않고, 읽기 lock과 쓰기 lock이 충돌할 때만 쓰기가 끝난 후에 읽기 lock을 걸어야만 한다.

2) Condition

자신이 원하는 쓰레드를 골라서 통지할 수 있다.wait(), notify() 단점 보완

wait()과 notify()로 공유 객체의 waiting pool에 같이 몰아넣는 대신, 각각 종류의 쓰레드를 위한 Condition을 만들어서 각각의 waiting pool에서 따로 기다리도록 하면 문제는 해결된다.