원문 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 |