Thread覚え書き

フレームワークばっかり使っているとこういうことを忘れてしまうので、試したことのメモです。
まずはThreadを3つ作って、動かすには

public class MyRunnableCounter implements Runnable {
    private int count = 0;
    
    public void run() {
        String name = Thread.currentThread().getName();
        for (int i=0; i<3; i++) {
            System.out.println(name + "(" + i + "): " + count++);
        }
    }
    
    public static void main(String[] args) {
        MyRunnableCounter counter = new MyRunnableCounter();
        Thread alice = new Thread(counter, "Alice");
        Thread bob = new Thread(counter, "Bob");
        Thread chris = new Thread(counter, "Chris");
        alice.start();
        bob.start();
        chris.start();
    }
}

のようにjava.lang.Runnableメソッドを実装したクラスを定義して、ThreadオブジェクトをnewするときにRunnableなオブジェクトを動かしてもらうようにお願いします。ところが、このままでは

Alice(0): 0
Alice(1): 1
Alice(2): 2
Bob(0): 3
Bob(1): 4
Bob(2): 5
Chris(0): 6
Chris(1): 7
Chris(2): 8

のようになってしまって、並行的に動きません。そこで、run()メソッドの中でThread.sleep()を実行して、別のスレッドが動けるようにしてあげます。

public void run() {
        String name = Thread.currentThread().getName();
        for (int i=0; i<3; i++) {
            System.out.println(name + "(" + i + "): " + count++);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
        }
    }

すると、

Alice(0): 0
Bob(0): 1
Chris(0): 2
Alice(1): 3
Bob(1): 4
Chris(1): 5
Alice(2): 6
Bob(2): 7
Chris(2): 8

のように、順番に動いてくれました。
次に、3つのスレッドが同時に動くとなると考えないといけないのがスレッド間の競合です。むりやり、競合が起こるような状況を作ってみました。これは、3人が水をバケツで汲みにいくプログラムですが、十分な水があるのを確認して安心して一休みする、そして水を汲む、ようになっています。当然、休んでいるうちに誰かが水を汲みだしてしまうわけです。

public class MyRunnableBucket implements Runnable {
    private Pool pool = new Pool();
    
    public void run() {
        for (int i=0; i<5; i++) {
            draw(4.8);
        }
    }
    
    void draw(double quantity) {
        String name = Thread.currentThread().getName();
        if (pool.getLeftWater() >= quantity) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
            pool.drawWater(quantity);
            System.out.println(name + " drew " + quantity + ". " +
                    pool.getLeftWater() + " of water in the pool.");
        } else {
            System.out.println("No enough water for " + name);
        }
    }
    
    public static void main(String[] args) {
        MyRunnableBucket b = new MyRunnableBucket();
        Thread alice = new Thread(b, "Alice");
        Thread bob = new Thread(b, "Bob");
        Thread chris = new Thread(b, "Chris");
        alice.start();
        bob.start();
        chris.start();
    }
}

class Pool {
    private double water = 30.0;
    
    double getLeftWater() {
        return water;
    }
    
    void drawWater(double quantity) {
        water -= quantity;
    }
}

このプログラムを実行すると

Alice drew 4.8. 15.599999999999998 of water in the pool.
Chris drew 4.8. 15.599999999999998 of water in the pool.
Bob drew 4.8. 15.599999999999998 of water in the pool.
Alice drew 4.8. 10.799999999999997 of water in the pool.
Chris drew 4.8. 5.999999999999997 of water in the pool.
Bob drew 4.8. 1.1999999999999975 of water in the pool.
No enough water for Bob
No enough water for Bob
No enough water for Bob
Alice drew 4.8. -3.6000000000000023 of water in the pool.
No enough water for Alice
No enough water for Alice
Chris drew 4.8. -8.400000000000002 of water in the pool.
No enough water for Chris
No enough water for Chris

というように、明らかに残っている水の量がおかしいので、スレッドの競合が起きていることがわかります。そこで競合を避けるために、synchronizedブロックを設けますが、Thread.sleep()の場所も変えないと、誰か一人だけが全部汲みだして終わることになるので、このように修正しました。

    public void run() {
        for (int i=0; i<4; i++) {
            draw(4.8);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
        }
    }
    
    void draw(double quantity) {
        String name = Thread.currentThread().getName();
        synchronized (Pool.class) {
            if (pool.getLeftWater() >= quantity) {
                pool.drawWater(quantity);
                System.out.println(name + " drew " + quantity + ". " +
                        pool.getLeftWater() + " of water in the pool.");
            } else {
                System.out.println("No enough water for " + name);
            }
        }
    }

こうすると、

Alice drew 4.8. 25.2 of water in the pool.
Bob drew 4.8. 20.4 of water in the pool.
Chris drew 4.8. 15.599999999999998 of water in the pool.
Alice drew 4.8. 10.799999999999997 of water in the pool.
Bob drew 4.8. 5.999999999999997 of water in the pool.
Chris drew 4.8. 1.1999999999999975 of water in the pool.
No enough water for Alice
No enough water for Bob
No enough water for Chris
No enough water for Alice
No enough water for Bob
No enough water for Chris

のように出力されて、スレッド間の競合を避けられたことがわかります。
では、synchronizedすれば安心ということで、なんでもかんでもsynchronizedにしても大丈夫でしょうか。プログラムの作り方によってはデッドロック状態に陥ります。わざとデッドロックに陥るプログラムを作ってみました。これは、水汲み場は一つ、バケツは二つ、働く人が三人いて、バケツを一つ借りて水を汲み、疲れたので一休みしてバケツを返すプログラムです。バケツが無かったら空くまで待ちます。

public class MyRunnableWorker implements Runnable {
    private SharedPool pool = new SharedPool();
    private Buckets buckets = new Buckets();
    
    public void run() {
        for (int i=0; i<4; i++) {
            synchronized (Buckets.class) {
                rentOut();
                synchronized (SharedPool.class) {
                    draw(4.8);
                }
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
        
            synchronized (Buckets.class) {
                returnBucket();
            }
        }
    }

    void draw(double quantity) {
        String name = Thread.currentThread().getName();
        if (pool.getLeftWater() >= quantity) {
            pool.drawWater(quantity);
            System.out.println(name + " drew " + quantity + ". " +
                    pool.getLeftWater() + " of water in the pool.");
        } else {
            System.out.println("No enough water for " + name);
        }
    }
    
    void rentOut() {
        String name = Thread.currentThread().getName();
        System.out.println(name +" is trying to get a bucket.");
        while (true) {
            if (buckets.getLeftBuckets() >= 1) {
                buckets.rentBucket();
                System.out.println(name + " rent out. " +
                            buckets.getLeftBuckets() + " left.");
                return;
            } 
        }
    }
    
    void returnBucket() {
        String name = Thread.currentThread().getName();
        buckets.returnBucket();
        System.out.println(name + " returned. " + buckets.getLeftBuckets() + " left.");
    }
    
    public static void main(String[] args) {
        MyRunnableWorker w = new MyRunnableWorker();
        Thread alice = new Thread(w, "Alice");
        Thread bob = new Thread(w, "Bob");
        Thread chris = new Thread(w, "Chris");
        alice.start();
        bob.start();
        chris.start();
    }
}

class SharedPool {
    private double water = 30.0;
    
    double getLeftWater() {
        return water;
    }
    
    void drawWater(double quantity) {
        water -= quantity;
    }
}

class Buckets {
    private int buckets = 2;
    
    int getLeftBuckets() {
        return buckets;
    }
    
    void rentBucket() {
        buckets--;
    }
    
    void returnBucket() {
        buckets++;
    }
}

どこにsynchronizedブロックを設けても、Buckets.classをロックしてしまうとデッドロック状態に陥ります。

Alice is trying to get a bucket.
Alice rent out. 1 left.
Alice drew 4.8. 25.2 of water in the pool.
Bob is trying to get a bucket.
Bob rent out. 0 left.
Bob drew 4.8. 20.4 of water in the pool.
Chris is trying to get a bucket.
(もうこれ以上進みません)

AliceとBobがバケツを返したくても、バケツの空きをまっているChrisがロックしてしまっているので返せないわけです。whileループで無理矢理待つのがそもそも問題ではありますが、水汲み場を使えるのは一度にひとりということで、Pool.classにロックをかけた中でバケツを借りるようにrun()メソッドを修正してみました。

    public void run() {
        for (int i=0; i<3; i++) {
            synchronized (SharedPool.class) {
                rentOut();    
                draw(4.8);
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
        
            synchronized (Buckets.class) {
                returnBucket();
            }
        }
    }

すると、

Alice is trying to get a bucket.
Alice rent out. 1 left.
Alice drew 4.8. 25.2 of water in the pool.
Bob is trying to get a bucket.
Bob rent out. 0 left.
Bob drew 4.8. 20.4 of water in the pool.
Chris is trying to get a bucket.
Alice returned. 1 left.
Chris rent out. 0 left.
Chris drew 4.8. 15.599999999999998 of water in the pool.
Alice is trying to get a bucket.
Bob returned. 1 left.
Alice rent out. 0 left.
Alice drew 4.8. 10.799999999999997 of water in the pool.
Bob is trying to get a bucket.
Alice returned. 1 left.
Bob rent out. 0 left.
Bob drew 4.8. 5.999999999999997 of water in the pool.
Alice is trying to get a bucket.
Bob returned. 1 left.
Alice rent out. 0 left.
Alice drew 4.8. 1.1999999999999975 of water in the pool.
Bob is trying to get a bucket.
Chris returned. 1 left.
Bob rent out. 0 left.
No enough water for Bob
Chris is trying to get a bucket.
Bob returned. 1 left.
Chris rent out. 0 left.
No enough water for Chris
Chris returned. 1 left.
Chris is trying to get a bucket.
Chris rent out. 0 left.
No enough water for Chris
Alice returned. 1 left.
Chris returned. 2 left.

のように、どころどころでバケツの空き待ちの状態を待ちながら、矛盾なく動くようになりました。