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

원문 URL : http://tutorials.jenkov.com/java-concurrency/semaphores.html



세마포어는 락과 마찬가지로 쓰레드간 신호 실종을 방지하기 위한 신호를 보내거나, 크리티컬 섹션을 보호하는 등의 목적을 위해 사용되는 쓰레드 동기화 구조이다. Java 5 는 java.util.concurrent 패키지에 세마포어 구현체를 포함하고 있기 때문에 직접 세마포어를 구현할 필요는 없다. 하지만 역시나 세마포어 구현의 기반 이론을 배우는 일은 충분히 가치있을 것이다.


Java 5 에는 Semaphore 클래스가 있다. 이 클래스에 대해서는 java.util.concurrent 튜토리얼의 java.util.concurrent.Semophore(원문) 을 읽어보기 바란다.



간단한 세마포어


아래는 간단한 Semaphore 구현이다.


public class Semaphore {
  private boolean signal = false;

  public synchronized void take() {
    this.signal = true;
    this.notify();
  }

  public synchronized void release() throws InterruptedException{
    while(!this.signal) wait();
    this.signal = false;
  }

}


take() 메소드는 Semaphore 내부에 신호 저장된 신호를 보낸다. release() 메소드는 신호를 기다린다. 신호가 도착하면 release() 메소드는 종료된다.


이러한 세마포어를 사용함으로써 신호 실종을 방지할 수 있다. notify() 대신 take() 를 호출하고, wait() 대신 release() 를 호출한다. release() 호출 전에 take() 호출이 발생하면, 신호는 내부적으로 signal 변수 안에 저장되기 때문에 release() 를 호출하는 쓰레드는 take() 이 호출되었었음을 알 수 있다. wait() 과 notify() 호출과는 다르다.


신호를 위해 세마포어를 사용할 때, take() 과 release() 라는 메소드 이름이 이상하게 보일 수 있다. 이 이름들은 세마포어를 락으로 사용함에서 나온 것이다. 이에 대한 설명은 후에 이어진다. 



신호를 위한 세마포어 사용


아래는 두 쓰레드 간의 신호를 위해 Semaphore 를 사용하는 간단한 예제이다.


Semaphore semaphore = new Semaphore();

SendingThread sender = new SendingThread(semaphore);

ReceivingThread receiver = new ReceivingThread(semaphore);

receiver.start();
sender.start();
public class SendingThread {
  Semaphore semaphore = null;

  public SendingThread(Semaphore semaphore){
    this.semaphore = semaphore;
  }

  public void run(){
    while(true){
      //do something, then signal
      this.semaphore.take();

    }
  }
}
public class RecevingThread {
  Semaphore semaphore = null;

  public ReceivingThread(Semaphore semaphore){
    this.semaphore = semaphore;
  }

  public void run(){
    while(true){
      this.semaphore.release();
      //receive signal, then do something...
    }
  }
}



카운팅 세마포어


앞서 선보인 Semaphore 구현에는 task() 메소드 호출로 발생한 신호의 수를 세지 않는다. 이 수를 세기 위해 코드를 조금 수정한다.이 클래스는 카운팅 세마포어라 부른다. 


public class CountingSemaphore {
  private int signals = 0;

  public synchronized void take() {
    this.signals++;
    this.notify();
  }

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

}



바운디드 세마포어


CountingSemaphore 는 신호의 최대치에 대한 제한이 없다. 이 제한을 위해 코드를 수정한다.


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();
  }
}


take() 메소드를 보자. 저장된 신호의 수가 한도에 다다르면 호출 쓰레드는 블록된다. release() 가 호출될 때까지 take() 호출 쓰레드는 신호를 보낼 수 없다. 



세마포어를 락으로 사용하기


바운디드 세마포어를 락으로 사용할 수 있다. 이를 위해서는 신호 최대치를 1 으로 세팅해야 한다. 그리고 take() 와 release() 호출을 크리티컬 섹션에 사용한다.


BoundedSemaphore semaphore = new BoundedSemaphore(1);

...

semaphore.take();

try{
  //critical section
} finally {
  semaphore.release();
}


세마포어를 신호를 주고받기 위해 사용할 때와는 달리, take() 와 release() 메소드는 이제 같은 쓰레드에 의해 호출된다. 오직 한 쓰레드만이 세마포어를 획득할 수 있기 때문에, take() 를 호출하는 다른 모든 쓰레드들은 release() 가 호출될 때까지 블록된다. release() 호출은 절대 블록되지 않는데, 이는 언제나 take() 호출이 먼저 발생하기 때문이다.


바운디드 세마포어는 어떤 영역으로 동시에 진입 가능한 쓰레드의 수를 제한하는 데에 사용될 수 있다. 예를 들어, 위 예제에서 BoundedSemaphore 의 신호 최대치를 5 로 세팅하면 어떨까? 5 개의 쓰레드가 동시에 크리티컬 섹션으로 진입할 수 있다. 물론 이 경우에는 5 개 쓰레드가 크리티컬 섹션에 동시에 접근해서 어플리케이션에 문제가 발생하지 않아야 한다는 전제가 필요하다.


finally 절에서의 release() 메소드 호출은 크리티컬 섹션에서 예외가 던져지더라도 반드시 releas() 가 호출되도록 보장하기 위함이다.





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

쓰레드 풀(Thread Pools)  (0) 2017.05.22
블로킹 큐(Blocking Queues)  (3) 2017.05.22
재진입 락아웃(Reentrance Lockout)  (0) 2017.05.21
읽기/쓰기 락(Read/Write Locks in Java)  (0) 2017.05.16
자바의 락(Locks in Java)  (0) 2017.05.09

+ Recent posts