A little bit of everything

元・情報系大学院生の備忘録

ReentrantLock の lock と tryLock

ReentrantLockとは

排他制御Javaで実装するときに使用するLockインタフェースの実装で、ロックを取得するタイミング、解放するタイミングなどを明示的に指定することできます。

ちなみに、このようにロックの取得/解放タイミングをプログラマが自由に指定できるものを明示的ロック(Explicit Lock)といいます。
その反対に、synchronized文などを使用したロックは、プログラマが明示的にロックの取得/解放を意識せずに排他制御を実現できることから、暗黙的ロック(Implicit Lock)と呼ばれます。

synchronized文はメソッドをまたがったロック処理(メソッドAの中でロックを取得して別のメソッドBの中で解放する、みたいな処理)は書けませんが、ReentrantLockはその辺を自由にプログラマが指定できるので、synchronizedよりも自由度の高い、複雑な排他制御が書けます。

ReentrantLockを使用したロック方法

ReentrantLockクラスには、ロックを取得するためのメソッドとして、

  • lockメソッド
  • tryLockメソッド

の2つが用意されています。

lockメソッドの動作

lockメソッドを呼ぶと、他スレッドが誰もロックをしていなければロックを取得できます。ただし、他スレッドが既にロックを取得していたとしたら、そのロックが解放されるまで待機します。したがって、既にロックを取得している他スレッドの処理に時間がかかれば、その分だけ待機しなくてはなりません。

ReentrantLock (Java Platform SE 8)


tryLockメソッドの動作

tryLockメソッドもlockメソッド同様、他スレッドが誰もロックをしていなければロックを取得できます。ただし、他スレッドが既にロックを取得していたらfalseを返却し、呼び出し側に制御を返します。したがって、lockメソッドと違い、tryLockはロックを取得している他スレッドの処理が終わるまで待機はしません。
ただし、tryLockに引数を渡すことで、ロックが取得できなかったときに、他スレッドがロックを解放するまで指定した時間だけ待機させることもできます。

ReentrantLock (Java Platform SE 8)


トイレを待つ人の例

lockメソッドを使った場合

lockとtryLockの動きについて、サンプルを書いてみました!
シンゴ、ツヨシ、ゴロウの3人が、1つしかないトイレを使いたがっているとします。誰か1人がトイレを使っている間は、他の2人はトイレを使うことができません。トイレに入れた1人だけが用を足すことができるとします。

ReentrantLockを使うと、シンゴ、ツヨシ、ゴロウの3人がそれぞれ独立したスレッドでも、常に1人だけがトイレに入って用を足し、他の2人はトイレが空くのを待つ、という状況を実装できます。

まずは、トイレを表すクラスを作ります。トイレは、外部からロックを取得/解放させるためのインタフェースを備えています。

/** トイレを表すクラス */
class Restroom {
    // トイレのロックを表すオブジェクト
    private final ReentrantLock lock = new ReentrantLock();

    // トイレをロックする
    public void lock() {
        lock.lock();
    }
    
    // トイレのロックを解除する
    public void unlock() {
        lock.unlock();
    }
}


次に、トイレに向かう人間を表すクラスを作ります。
トイレに入ってロックする→用をたす→トイレから出る(トイレのロックを解放する) 、という動きをします。

/** 人間を表すクラス */
class People {
    protected String name;

    public People(String _name) {
        this.name = _name;
    }
}

/** トイレに向かう人間を表すクラス(Runnableを実装)  */
class PeopleGoingWC extends People implements Runnable {
    //目的のトイレ
    private Restroom targetRestRoom;

    public PeopleGoingWC(String name, Restroom _targetRestRoom) {
        super(name);
        this.targetRestRoom = _targetRestRoom;
    }

    //トイレに向かう人間のメインの動作
    @Override
    public void run() {
        try {
            //トイレに入ってロックする。
            targetRestRoom.lock();
            System.out.println(name + ": トイレに入りました!");
            
            //用をたす。
            relieve();
        } finally {
            //トイレから出る。
            System.out.println(name + ": トイレから出ました。");
            targetRestRoom.unlock();
        }
    }

    // 用をたすメソッド
    private void relieve() {
        //4秒待って、「ふぅ...」とつぶやくだけ。
        try {
            Thread.sleep(4000);
            System.out.println(name + ": ふぅ...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


最後に、mainメソッドの中でトイレをたった1つだけ作り、シンゴ、ツヨシ、ゴロウのスレッドをスタートさせてみます。

public class Main {
    public static void main(String[] args) {
        Restroom restRoom = new Restroom();
        new Thread(new PeopleGoingWC("シンゴ", restRoom)).start();
        new Thread(new PeopleGoingWC("ツヨシ", restRoom)).start();
        new Thread(new PeopleGoingWC("ゴロウ", restRoom)).start();
    }
}


実行結果は以下のようになります。シンゴ、ツヨシ、ゴロウは必ず同時に1人しかトイレに入って用を足してないことがわかります!

シンゴ: トイレに入りました!
シンゴ: ふぅ...
シンゴ: トイレから出ました。
ツヨシ: トイレに入りました!
ツヨシ: ふぅ...
ツヨシ: トイレから出ました。
ゴロウ: トイレに入りました!
ゴロウ: ふぅ...
ゴロウ: トイレから出ました。

1人が用をたしているときは他の2人はトイレのロックが解除されるのを待っていて、ロックが解放されたら次の人がトイレのロックを取得して、・・・という動作になるので、うまく排他制御ができています。


ちなみに、run()の中でロックが取得/解除をしている部分を以下のようにコメントアウトしてしまい、排他制御をしないとするとどうなるでしょうか?

@Override
    public void run() {
        try {
            //targetRestRoom.lock();
            System.out.println(name + ": トイレに入りました!");
            
            //用をたす。
            relieve();
        } finally {
            System.out.println(name + ": トイレから出ました。");
            //targetRestRoom.unlock();
        }
    }


実行結果↓

ゴロウ: トイレに入りました!
シンゴ: トイレに入りました!
ツヨシ: トイレに入りました!
シンゴ: ふぅ...
シンゴ: トイレから出ました。
ゴロウ: ふぅ...
ツヨシ: ふぅ...
ツヨシ: トイレから出ました。
ゴロウ: トイレから出ました。


みんなで一斉にトイレに入って用を足して出ていく、みたいになります(笑)


tryLockメソッドを使った場合

次に、tryLockメソッドの例を説明します。
上で挙げたコード例のRestroomクラスを以下のように書き換えます。

/** トイレを表すクラス */
class Restroom {
    // トイレのロックを表すオブジェクト
    private final ReentrantLock lock = new ReentrantLock();

    // トイレのロックを取得する。取得できない場合、指定した時間の間だけ継続してリトライする。
    public boolean tryLock(long waitTimeSec) throws InterruptedException {
        return lock.tryLock(waitTimeSec, TimeUnit.SECONDS);
    }

    // トイレのロックを解除する
    public void unlock() {
        lock.unlock();
    }
}

ロックするときにtryLockを使うようにして、トイレのロックが解放されるのを指定した時間の間だけ待てるように変更しています。

次に、トイレに向かう人間クラスを以下のように変更しました。

/** トイレに向かう人間を表すクラス(Runnableを実装)  */
class PeopleGoingWC extends People implements Runnable {
    private long canWaitTimeSec; //トイレが空いてないときに我慢できる秒数
    private Restroom targetRestRoom;

    public PeopleGoingWC(String name, long _canWaitTimeSec, Restroom _targetRestRoom) {
        super(name);
        this.canWaitTimeSec = _canWaitTimeSec;
        this.targetRestRoom = _targetRestRoom;
    }

    //トイレに向かう人間のメインの動作
    @Override
    public void run() {
        //トイレのロックが取得できるか、canWaitTimeSecの間トライする。
        if (tryToGoInside()) {
            //トイレのロックが取得できた場合
            System.out.println(name + ": " + "トイレが空いているので入ります!");
            
            // 用をたす。
            relieve();
            
            // トイレから出る。
            System.out.println(name + ": トイレを出ます。");
            targetRestRoom.unlock();
        } else {
            //トイレのロックが取得できず、我慢もできなかった場合
            System.out.println(name + ": " + canWaitTimeSec + "秒間待ったけどトイレが空かなかったので諦めます(T_T)");
        }

    }

    //トイレの中に入る
    private boolean tryToGoInside() {
        try {
            return targetRestRoom.tryLock(canWaitTimeSec);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    // 用をたすメソッド
    private void relieve() {
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name + ": ふぅ...");
    }
}


最後に、mainメソッドの中でトイレをたった1つだけ作り、シンゴ、ツヨシ、ゴロウのスレッドをスタートさせてみます。

public class Main {
    public static void main(String[] args) {
        Restroom restRoom = new Restroom();
        
        //シンゴはトイレが解放されるまで5秒間待てる。
        new Thread(new PeopleGoingWC("シンゴ", 5, restRoom)).start();
        
        //ツヨシはトイレが解放されるまで4秒間待てる。
        new Thread(new PeopleGoingWC("ツヨシ", 4, restRoom)).start();
        
        //ゴロウはトイレが解放されるまで3秒間待てる。
        new Thread(new PeopleGoingWC("ゴロウ", 3, restRoom)).start();
    }
}


実行結果は以下のようになります。シンゴ、ツヨシ、ゴロウは必ず同時に1人しかトイレに入って用を足していません。
ゴロウに関しては3秒間トイレが空くのを待ったけど諦めていることが分かります。

ツヨシ: トイレが空いているので入ります!
ゴロウ: 3秒間待ったけどトイレが空かなかったので諦めます(T_T)
ツヨシ: ふぅ...
ツヨシ: トイレを出ます。
シンゴ: トイレが空いているので入ります!
シンゴ: ふぅ...
シンゴ: トイレを出ます。


何度か実行してみると、表示される結果も変わります。
シンゴ、ツヨシ、ゴロウの誰が最初にトイレのロックを取得するかによって、結果が変わってくるためです。

ゴロウ: トイレが空いているので入ります!
ゴロウ: ふぅ...
ツヨシ: 4秒間待ったけどトイレが空かなかったので諦めます(T_T)
ゴロウ: トイレを出ます。
シンゴ: トイレが空いているので入ります!
シンゴ: ふぅ...
シンゴ: トイレを出ます。


シンゴ: トイレが空いているので入ります!
ゴロウ: 3秒間待ったけどトイレが空かなかったので諦めます(T_T)
ツヨシ: 4秒間待ったけどトイレが空かなかったので諦めます(T_T)
シンゴ: ふぅ...
シンゴ: トイレを出ます。