[java]new Semaphore(0)

とあるコードの中に、以下のような記述がありました。

import java.util.concurrent.Semaphore;
...
    Semaphore permit = new Semaphore(0);

これまで、ミューテックスは1つの、セマフォは任意の数のクリティカルセクションへの進入を許可するもの、と覚えていました。ですので、初期値に「0」が指定されたセマフォがどういった意味を持つのか理解できず調べてみました。その中で、意外にセマフォ自体を理解できていないことが分かり、認識の弱かった部分を纏めてみました。

所有(アカウント)という概念がない

ミューテックスの場合は獲得したタスク(スレッド)がそのミューテックスを所有しますが、セマフォには所有されるという概念がありません。以下のコードを実行し、スレッドダンプを取ってみました。

import java.util.concurrent.Semaphore;

import org.junit.Test;

public class SemaphoreTest {

    @Test
    public void trySemaphore() throws InterruptedException{
        final Semaphore permit = new Semaphore(2);
        Runnable task = new Runnable(){
            public void run() {
                try {
                    permit.acquire();
                    Thread.sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally{
                    permit.release();
                }
            }
        };
        
        Thread t1 = new Thread(task, "t1");
        Thread t2 = new Thread(task, "t2");
        Thread t3 = new Thread(task, "t3");
        
        t1.start();
        t2.start();
        t3.start();

        t1.join();
        t2.join();
        t3.join();
    }
}

上記コードの実行中にスレッドダンプを取ってみると、以下のようになります。

Full thread dump Java HotSpot(TM) Client VM (21.0-b17 mixed mode, sharing):

"t3" prio=6 tid=0x046cfc00 nid=0xb00 waiting on condition [0x04c9f000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x24b57978> (a java.util.concurrent.Semaphore$NonfairSync)
	at java.util.concurrent.locks.LockSupport.park(Unknown Source)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(Unknown Source)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(Unknown Source)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(Unknown Source)
	at java.util.concurrent.Semaphore.acquire(Unknown Source)
	at net.wrap_trap.test.concurrent.SemaphoreTest$1.run(SemaphoreTest.java:15)
	at java.lang.Thread.run(Unknown Source)

   Locked ownable synchronizers:
	- None

"t2" prio=6 tid=0x046cf400 nid=0x308 waiting on condition [0x0489f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
	at java.lang.Thread.sleep(Native Method)
	at net.wrap_trap.test.concurrent.SemaphoreTest$1.run(SemaphoreTest.java:16)
	at java.lang.Thread.run(Unknown Source)

   Locked ownable synchronizers:
	- None

"t1" prio=6 tid=0x046bf000 nid=0x860 waiting on condition [0x04c1f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
	at java.lang.Thread.sleep(Native Method)
	at net.wrap_trap.test.concurrent.SemaphoreTest$1.run(SemaphoreTest.java:16)
	at java.lang.Thread.run(Unknown Source)

   Locked ownable synchronizers:
	- None
...

スレッドt1,t2はセマフォを獲得し、t3はセマフォを待っている状態ですが、「Locked ownable synchronizers:」は何れもNoneとなっています。
同じことをReentrantLockで試してみます。

Full thread dump Java HotSpot(TM) Client VM (21.0-b17 mixed mode, sharing):

"t3" prio=6 tid=0x047e6800 nid=0xc2c waiting on condition [0x04aef000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x24b57f50> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
	at java.util.concurrent.locks.LockSupport.park(Unknown Source)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(Unknown Source)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(Unknown Source)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Unknown Source)
	at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(Unknown Source)
	at java.util.concurrent.locks.ReentrantLock.lock(Unknown Source)
	at net.wrap_trap.test.concurrent.ReentrantLockTest$1.run(ReentrantLockTest.java:15)
	at java.lang.Thread.run(Unknown Source)

   Locked ownable synchronizers:
	- None

"t2" prio=6 tid=0x047e3400 nid=0x1204 waiting on condition [0x0466f000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x24b57f50> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
	at java.util.concurrent.locks.LockSupport.park(Unknown Source)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(Unknown Source)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(Unknown Source)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Unknown Source)
	at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(Unknown Source)
	at java.util.concurrent.locks.ReentrantLock.lock(Unknown Source)
	at net.wrap_trap.test.concurrent.ReentrantLockTest$1.run(ReentrantLockTest.java:15)
	at java.lang.Thread.run(Unknown Source)

   Locked ownable synchronizers:
	- None

"t1" prio=6 tid=0x0478ec00 nid=0x5d4 waiting on condition [0x04c0f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
	at java.lang.Thread.sleep(Native Method)
	at net.wrap_trap.test.concurrent.ReentrantLockTest$1.run(ReentrantLockTest.java:16)
	at java.lang.Thread.run(Unknown Source)

   Locked ownable synchronizers:
	- <0x24b57f50> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
...

スレッドt1はロックを獲得しており、「Locked ownable synchronizers:」には獲得しているロックの情報が表示されます。セマフォは獲得したスレッドには関連せず、獲得できる数を管理しているだけです。

どのスレッドからも解放できる

セマフォは特定のスレッドと関連しない為、どのスレッドからでもセマフォを解放することができます。例えば以下のようなコード。

import java.util.Random;
import java.util.concurrent.Semaphore;

import org.junit.Test;

public class ConsumerProducer {

    static class Value {
        public Object value;
    }
    
    @Test
    public void tryProvide() throws InterruptedException {
        final Semaphore s = new Semaphore(0);
        final Value shared = new Value();
        final Random rand = new Random();
        
        class Consumer implements Runnable {
            public void run() {
                while(true) {
                    try {
                        s.acquire();
                        System.out.println("get: " + shared.value);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }            
        }
        
        class Producer implements Runnable {
            public void run() {
                while(true) {
                    shared.value = rand.nextInt();
                    System.out.println("put: " + shared.value);
                    s.release();
                    try {
                        Thread.sleep(3000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }                        
        }
    
        Thread t1 = new Thread(new Consumer());
        t1.start();
        
        Thread t2 = new Thread(new Producer());
        t2.start();
        
        t1.join();
        t2.join();
    }
}

Customerのスレッドはセマフォを獲得するだけ、Producerのスレッドはセマフォを解放するだけです。つまり、セマフォを獲得したスレッドとは異なるスレッドでセマフォを解放しています。上記コードでは「new Semaphore(0);」としていて、最初にCustomerのスレッドがセマフォを獲得しようとするのですが、Producerが値を準備してセマフォを解放するまでセマフォを獲得することができません。

もちろんObject#wait、Object#notifyを使って同じようなことはできますが、synchronizedブロックを必要としないセマフォの方が書き方としてはスマートだと思います。但し、前記のとおりセマフォには所有という考え方が無い為、例えばセマフォの獲得で長い時間待たされた場合、どのスレッドがセマフォを解放できていないのかスレッドダンプで確認しようとしても、セマフォを獲得しているスレッドを特定することはできません。この点は注意が必要です。