이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/anatomy-of-a-synchronizer.html



많은 동기화 장치(락, 세마포어, 블로킹 큐, 기타 등등)에 기능적 차이가 있더라도, 이 동기화 장치들의 내부 설계는 모두 유사하게 이루어져 있다. 다시 말해서 이 장치들은 내부적으로 같은(또는 유사한) 기본 부분들로 구성되어 있다. 이 기본 부분들에 대한 지식은 동기화 장치를 설계할 때 큰 도움이 될 수 있다. 이 글은 이 기본 부분들에 대해 다룬다.


※ 본문의 내용은 2004년 봄 Jakob Jenkov, Toke Johansen, Lars Bjørn 코펜하겐 IT 대학 이학 석사과정 프로젝트 결과물의 일부분이다. 이 프로젝트를 진행하는 동안 우리는 Doug Lea 에게 프로젝트 관련 연구에 대한 자문을 구했다. 흥미롭게도 그는 이 프로젝트와는 독립적으로, Java 5 의 동시성 유틸리티 개발 과정에서 비슷한 결론을 내놓았다.  Doug Lea 의 연구는 책 'Java Concurrency in Practice' 에 설명되어 있다. 또한 이 책은 '동기화 장치 해부' 챕터를 담고 있는데, 챕터의 내용은 본문의 그것과 유사하지만 완전히 같지는 않다.


모두는 아닐지라도, 대부분의 동기화 장치의 목적은 코드의 특정한 부분(크리티컬 섹션)을 쓰레드들의 동시 접근으로부터 보호하는 데에 있다. 이를 위해 동기화 장치는 다음의 각 부분들을 필요로 한다.


  1. 상태
  2. 접근 조건
  3. 상태 변경
  4. 통지 전략
  5. 확인-설정 메소드
  6. 설정 메소드


이 모든 부분들을 정확히 똑같이 가지고 있는 동기화 장치가 있을 수도 있지만, 모든 동기화 장치가 이 부분들을 모두 가지고 있는 것은 아니다. 보통 동기화 장치에는 이들 중 하나 이상의 부분들이 있다.



상태


동기화 장치의 상태는 쓰레드가 접근 권한을 획득할 수 있는지 결정하기 위한 접근 조건에 사용된다. Lock 에서의 상태는 boolean 타입으로, Lock 이 잠겼는지 잠기지 않았는지를 나타낸다. BoundedSemaphore 는 내부에 카운터(int) 와 최대치(int) 라는 상태를 둔다. 카운터는 세마포어를 획득한 쓰레드의 수이고, 최대치는 쓰레드가 획득 가능한 최대(제한) 세마포어의 수이다. 블로킹 큐에서는 큐의 엘레멘트 목록과 큐 최대 크기(int) 를 상태로 둔다.


다음은 Lock 과 BoundedSemaphore 의 코드 일부이다. 상태 코드는 볼드 처리되었다.


public class Lock{

  //state is kept here
  private boolean isLocked = false; 

  public synchronized void lock()
  throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }

  ...
}
public class BoundedSemaphore {

  //state is kept here
      private int signals = 0;
      private int bound   = 0;
      
  public BoundedSemaphore(int upperBound){
    this.bound = upperBound;
  }

  public synchronized void take() throws InterruptedException{
    while(this.signals == bound) wait();
    this.signal++;
    this.notify();
  }
  ...
}



접근 조건


접근 조건은 확인-설정 메소드를 호출하는 쓰레드가 상태를 설정할 수 있도록 허용할지를 판단하는 조건이다. 접근 조건은 일반적으로 동기화 장치의 상태에 기반을 둔다. 보통 while 루프 안에서 접근 조건을 이용하여 상태가 조건에 맞는지를 확인하는 방법으로 Spurious Wakeups 에 대응한다. 접근 조건의 확인 결과는 true 또는 false 로 나타낸다.


Lock 에서의 접근 조건은 간단히 isLocked 멤버 변수의 값을 확인하는 것이다. BoundedSemaphore 에서의 접근 조건은 세마포어를 '획득하는지' 혹은 '해제하는지' 에 따른 두 가지 접근 조건이 있다. 쓰레드가 세마포어를 획득하려 하면 signals 변수의 값이 획득 상한값에 다다르지 않았는지 확인한다. 쓰레드가 세마포어를 해제하려 하면 signals 변수의 값이 0 이 아닌지 확인한다.


다음은 LockBoundedSemaphore 의 코드 일부이다. 접근 조건은 볼드 처리되었다. while 루프 안에서 접근 조건이 어떤식으로 작동하는지 알 수 있다.


public class Lock{

  private boolean isLocked = false;

  public synchronized void lock()
  throws InterruptedException{
    //access condition
    while(isLocked){
      wait();
    }
    isLocked = true;
  }

  ...
}
public class BoundedSemaphore {
  private int signals = 0;
  private int bound   = 0;

  public BoundedSemaphore(int upperBound){
    this.bound = upperBound;
  }

  public synchronized void take() throws InterruptedException{
    //access condition
    while(this.signals == bound) wait();
    this.signals++;
    this.notify();
  }

  public synchronized void release() throws InterruptedException{
    //access condition
    while(this.signals == 0) wait();
    this.signals--;
    this.notify();
  }
}



상태 변경


쓰레드가 크리티컬 섹션으로의 접근을 획득하면 다른 쓰레드들의 접근을 막기 위해 동기화 장치의 상태가 변경되어야 한다. 다시 말해서, 상태는 현재 쓰레드가 크리티컬 섹션 안에서 실행되고 있음을 나타내야 하고, 이것은 동일한 크리티컬 섹션에 접근하려 하는 다른 쓰레드들의 접근 조건에 영향을 미치도록 한다.


Lock 에서의 상태 변경은 isLocked = true; 코드이다. 세마포어에서의 상태 변경은 signals--; 또는 signals++; 코드가 있다.


다음은 상태 변경을 보여주는 코드 일부이다. 상태 변경은 볼드 처리되었다.


public class Lock{

  private boolean isLocked = false;

  public synchronized void lock()
  throws InterruptedException{
    while(isLocked){
      wait();
    }
    //state change
    isLocked = true;
  }

  public synchronized void unlock(){
    //state change
    isLocked = false;
    notify();
  }
}
public class BoundedSemaphore {
  private int signals = 0;
  private int bound   = 0;

  public BoundedSemaphore(int upperBound){
    this.bound = upperBound;
  }

  public synchronized void take() throws InterruptedException{
    while(this.signals == bound) wait();
    //state change
    this.signals++;
    this.notify();
  }

  public synchronized void release() throws InterruptedException{
    while(this.signals == 0) wait();
    //state change
    this.signals--;
    this.notify();
  }
}



통지 전략


쓰레드가 동기화 장치의 상태를 변경하면, 때로는 대기 상태에 있는 다른 쓰레드들에게 변경 사실을 알릴 필요가 있다. 아마 이 상태 변경은 다른 쓰레드들의 접근 조건을 true 로 만들어 접근이 가능하게끔 해줄 것이다.


일반적인 통지 전략은 세 가지로 분류된다.


  1. 모든 대기 쓰레드에게 통지
  2. N 개의 대기 쓰레드 중 임의의 1 개 쓰레드에게 통지
  3. N 개의 대기 쓰레드 중 지정된 1 개 쓰레드에게 통지


모든 대기 쓰레드에게 통지하기는 상당히 쉽다. 대기 쓰레드들은 동일한 객체의 wait() 메소드를 호출한다. 이 객체의 notifyAll() 을 호출함으로써 대기 쓰레드들을 모두 깨울 수 있다.


임의의 1 개 쓰레드를 깨우기 또한 어렵지 않다. 대기 쓰레드들이 wait() 을 호출한 객체의 notify() 를 호출하면 된다. notify() 메소드는 대기중인 쓰레드들 중 어떤 쓰레드를 깨울지에 대한 보장이 전혀 없기 때문에 '임의의 1 개 쓰레드' 라는 요건을 충족한다.


때때로 대기 쓰레드 중 임의의 쓰레드가 아닌, 특정한 쓰레드를 깨워야 할 필요가 있다. 가령, 대기 쓰레드들의 접근 획득에 있어 특정한 순서가 보장되어야 할 때이다. 대기 상태로 들어간 순서 또는 별도로 지정된 우선순위로 쓰레드들의 접근 획득 순서를 조정해야 하는 것이다. 이런 경우에는 대기 쓰레드는 자신만의 구분된 객체의 wait() 를 호출해야 한다. 그리고 쓰레를 깨울 때는 대상 쓰레드가 wait() 을 호출한 객체의 nofity() 를 호출하여 정확히 해당 쓰레드를 깨우도록 한다. 관련 예제는 기아상태와 공정성 에서 찾을 수 있다.


다음은 통지 전략 코드의 일부이다. 통지 코드(임의의 1 개 쓰레드에게 통지)는 볼드 처리되었다.


public class Lock{

  private boolean isLocked = false;

  public synchronized void lock()
  throws InterruptedException{
    while(isLocked){
      //wait strategy - related to notification strategy
      wait();
    }
    isLocked = true;
  }

  public synchronized void unlock(){
    isLocked = false;
    notify(); //notification strategy
  }
}



확인-설정 메소드


대부분의 경우 동기화 장치는 두 가지 유형의 메소드가 있다. 첫 번째 유형은 확인-설정 메소드이다(두 번째는 설정 메소드인데, 다음 섹션에서 다룬다.). 확확인-설정이란 이 메소드를 호출하는 쓰레드는 동기화 장치의 내부 상태가 접근 조건을 충족하는지 확인한다. 상태가 조건을 충족한다고 확인되면, 쓰레드는 동기화 장치의 내부 상태에 쓰레드가 접근을 획득했음을 나타내도록 설정한다.


주로 이 상태 설정은 다른 쓰레드들의 접근을 막기 위해 접근 조건이 false 가 되도록 한다. 간혹 다른 경우도 있는데, 예를 들자면 읽기/쓰기 락이 그렇다. 읽기/쓰기 락에서 읽기 접근 쓰레드는 락의 상태를 변경하여 자신의 접근을 표시하지만, 이것이 다른 쓰레드들의 접근을 막지는 않는다. 쓰기 접근이 없는 한 다른 쓰레드들의 읽기 접근도 허용된다.


확인-설정 메소드에서 반드시 지켜져야 할 점은 확인-설정 연산이 반드시 원자적으로 실행되어야 한다는 것이다. 확인-설정 메소드가 실행될 때 '확인' 과 '설정' 사이에 다른 쓰레드가 끼어들어서는 안된다.


확인-설정 메소드의 흐름은 주로 다음 순서를 따른다.


  1. 필요하다면 확인 전에 상태를 설정한다.
  2. 상태를 접근 조건에 대해 확인한다.
  3. 접근 조건을 충족하지 못한다면, 대기한다.
  4. 접근 조건을 충족한다면, 상태를 설정한다. 그리고 필요하다면 대기 쓰레드에게 통지한다.


ReadWriteLock 클래스의 lockWrite() 메소드가 확인-설정 메소드의 예시가 될 수 있다. lockWrite() 호출 쓰레드는 확인 전에 상태를 설정한다(writeRequest++). 다음으로 canGrantWriteAccess() 메소드에서 내부 상태를 접근 조건에 대해 확인한다. 조건을 충족한다면 내부 상태를 설정하고 메소드는 종료된다. 아래 예제에서는 대기 쓰레드에게 통지하는 부분은 없다.


public class ReadWriteLock{
    private Map<Thread, Integer> readingThreads =
        new HashMap<Thread, Integer>();

    private int writeAccesses    = 0;
    private int writeRequests    = 0;
    private Thread writingThread = null;

    ...

    
        public synchronized void lockWrite() throws InterruptedException{
        writeRequests++;
        Thread callingThread = Thread.currentThread();
        while(! canGrantWriteAccess(callingThread)){
        wait();
        }
        writeRequests--;
        writeAccesses++;
        writingThread = callingThread;
        }
    

    ...
}


아래 BoundedSemaphore 클래스에는 두 개의 확인-설정 메소드가 있다. take() 와 release() 가 그것이다. 이 두 메소드는 저마다 내부 상태를 확인하고 설정한다.


public class BoundedSemaphore {
  private int signals = 0;
  private int bound   = 0;

  public BoundedSemaphore(int upperBound){
    this.bound = upperBound;
  }

  
      public synchronized void take() throws InterruptedException{
      while(this.signals == bound) wait();
      this.signals++;
      this.notify();
      }

      public synchronized void release() throws InterruptedException{
      while(this.signals == 0) wait();
      this.signals--;
      this.notify();
      }
  
}



설정 메소드


설정 메소드는 동기화 장치에서 자주 등장하는 메소드의 두 번째 유형이다. 설정 메소드는 동기화 장치의 내부 상태를 설정하는데, 여기에 확인 작업은 없다. 설정 메소드의 전형적인 예제는 Lock 클래스의 unlock() 메소드이다. 락을 소유한 메소드는 Lock 이 해제되었는지 확인할 필요 없이, 언제나 락을 해제할 수 있다. 


설정 메소드의 흐름은 주로 다음 순서를 따른다.


  1. 내부 상태를 설정한다.
  2. 대기 쓰레드에게 이를 알린다.


다음은 unlock() 메소드 예제이다.


public class Lock{

  private boolean isLocked = false;
  
      public synchronized void unlock(){
      isLocked = false;
      notify();
      }
  
}


'Java > Concurrency' 카테고리의 다른 글

암달의 법칙(Amdahl's Law)  (1) 2017.06.17
논 블로킹 알고리즘(Non-blocking Algorithms)  (2) 2017.05.27
컴페어 스왑(Compare and Swap)  (0) 2017.05.24
쓰레드 풀(Thread Pools)  (0) 2017.05.22
블로킹 큐(Blocking Queues)  (3) 2017.05.22

+ Recent posts