@blog.justoneplanet.info

日々勉強

スレッド

Thread thread = new Thread();

■Thread

以下のようにすることで互いに別スレッドで動作するプログラムを記述できる。

class PingPong extends Thread {
    private String mWord;
    private int mDelay;
    public PingPong(String word, int delay) {
        mWord  = word;
        mDelay = delay;
    }
    @Override
    public void run() {
        try {
            for(;;) {
                System.out.println(mWord);
                Thread.sleep(delay);// 自身が実行されているスレッドで次の時間まで待つ
            }
        }
        catch(InterruptException e) {
            return;// スレッドを終了させる
        }
    }

    // スタートする
    public static void main(String[] args) {
        new PingPong("ping", 33).start();
        new PingPong("PONG", 99).start();
    }
}

但し、以下のようにRunnableを使った記述のほうが一般的ではある。

Runnable

Runnableインタフェースを実装しThreadにセットすることができる。

class RunPingPong extends Runnable {
    private String mWord;
    private int mDelay;
    public PingPong(String word, int delay) {
        mWord  = word;
        mDelay = delay;
    }
    @Override
    public void run() {
        try {
            for(;;) {
                System.out.println(mWord);
                Thread.sleep(delay);// 自身が実行されているスレッドで次の時間まで待つ
            }
        }
        catch(InterruptException e) {
            return;// スレッドを終了させる
        }
    }

    // スタートする
    public static void main(String[] args) {
        Runnable ping = new RunPingPong("ping", 33);
        Runnable ping = new RunPingPong("PONG", 99);
        new Thread(ping).start();
        new Thread(pong).start();
    }
}

コンストラクタでスレッドを開始させるとコンストラクタが実行される前にスレッドがオブジェクトのフィールドにアクセスできてしまうため危険である。

class PrintServer implement Runnable {
    private final PrintQueue q = new PrintQueue();
    public PrintServer() {
        Runnable service = new Runnable() {
        }
        new Thread(this).start();
    }

    @Override
    public void run() {
        for(;;) {
            realPrint();
        }
    }

    public void print(Print job) {
        q.add(job);
    }
    
    public void realPrint(Print job) {
        // 実際の処理
    }
}

一般的には以下のように使用される。

class PrintServer {
    private final PrintQueue q = new PrintQueue();
    public PrintServer() {
        Runnable service = new Runnable() {
            @Override
            public void run() {
                for(;;) {
                    realPrint();
                }
            }
        }
        new Thread(service).start();
    }

    public void print(Print job) {
        q.add(job);
    }
    
    public void realPrint(Print job) {
        // 実際の処理
    }
}

スレッドは生成された時に自身への参照を自身のThreadGroupに保持する。


■同期

全てのオブジェクトはロックを持っている。そのロックはsynchronizedメソッドやsynchronized文を使用することにより獲得したり解放したりできる。

以下の例では、synchronizedされたアクセサによって残高が同期なしに修正できないように保護されている。

public class BankAccount {
    private long number;
    private long balance;// 残高
    public BankAccount(long initialDeposit) {
        balance = initialDeposit;
    }
    public synchronized long getBalance() {
        return balance;
    }
    public synchronized long deposit(long amount) {
        balance += amount;
    }
}

フィールドが外部から直接アクセス可能であった場合は同期を取る必要がない。

  • 各メソッドは、相互排他して実行される
  • synchronized宣言により、2つ以上の実行しているスレッドがお互いに干渉しないことが保証される
  • メソッド呼び出しの1つが実行を開始したら、他からは最初のメソッドの呼び出しが完了するまで開始できない
  • 操作の順序には保証はない
  • 拡張したクラスがsynchronizedメソッドをオーバーライドしたときにsynchronizedにしないこともできる

コンストラクタは同期をとる必要はないしsynchronizedを宣言できない

オブジェクトが生成される時だけ実行され新たなオブジェクトを生成する際には1つのスレッド内だけで実行される。

staticのsynchronizedメソッド

staticなフィールドのデータがスレッド間で共有されている場合、staticでsynchronizedされたメソッドからアクセスする必要がある。この時、自身に関連付けられたClassオブジェクトに対してロックがかかるが、synchronizedされたメソッドに対してのinvokeのみブロックの対象となる。

ロックの確認

以下のようにすることで現在のスレッドがオブジェクトに対してロックを保持しているか判定できる。

Thread.holdLock(this);

synchronized文

以下のようにすることでカレントオブジェクトではなく任意のオブジェクトに対してのロックを獲得できる。但し、exprはオブジェクトの参照を返す必要がある。

synchronized(expr) {
    statements
}

synchronizedメソッドはthisへの参照を伴うsynchronized文であり省略した表現である。

public static void abs(int[] values) {
    synchronized(values) {
        for(int i = 0; i < values.length; i++) {
            if(values[i] < 0){
                values[i] = -values[i];
            }
        }
    }
}
synchronized文のメリット
  • 必要な箇所に限定された(フィールドへの代入だけなどの)処理を定義しロックの時間を短くできる
  • 同クラス内でもthis以外の異なったオブジェクトで同期を取ることが可能

用例

エンクロージングオブジェクトに対して内部オブジェクトが同期を取る。

public class Outer {
    private int data;
    public class Inner {
        void setOuterData() {
            synchronized(Outer.this) {
                data = 1;
            }
        }
    }
}

コンストラクタ内で使用される。

class Body {
    Body() {
        synchronized(Body.class) {
            idNum = nextID++;
        }
    }
}

■設計

クラスに対して適切な同期を設計することは複雑で難しい。

  • synchronized文は全てのクライアントが正しい処理を行うことに依存するので(クライアント同期)、オブジェクトへのアクセスメソッドをsynchronizedとする方が強固である(サーバー同期)
  • 複数のメソッド呼び出しについてアトミックである必要がある場合、1つのメソッドに一連のメソッドをまとめる方法は設計の観点から一般的とはいえない
  • 複数のメソッド呼び出しについてアトミックである必要がある場合、synchronized文でクライアント側同期しか選択肢がない場合がある
class Util {
    private int value;
    Util(int initial) {
        value = initial;
    }
    public synchronized add(int x) {
        value += x;
        System.out.println("value:" + value);
    }
}
// Util util = new Util(5);
// thread a : util.add(7);
// thread b : util.add(9);
// 5 12 21
// 5 14 21
class Util {
    private static int value = 0;
    public synchronized static add(int x) {
        value += x;
        System.out.println("value:" + value);
    }
}
// thread a : Util.add(7);
// thread b : Util.add(9);
// 0 7 16
// 0 9 16
class Util {
    private static int value = 0;
    public synchronized static add(int x) {
        value += x;
        System.out.println("value:" + value);
    }
    public static subtract(int x) {
        add(-x);
    }
}

■スレッド間通信

wait

条件を待つスレッドは以下のように記述する。

synchronized void doWhenCondition() {
    while(!condition) {// whileをifにしてはいけない
        wait();
    }
    // 処理
}

スレッドを(一時)停止させるときにwaitはオブジェクトのロックをアトミックに解放する。(スレッドを停止した場合にロックの解放までがアトミックである)

notifyAll, notify

以下のようにして他のスレッドに通知する。

synchronized void changeCondition() {
    // 条件で使用される値の変更処理
    notifyAll();// notify();
}

notifyAllを使用するのは以下の条件のときである。

  • 同じ条件に対して全てのスレッドが待っている
  • 変更された条件で作用があるのは1つのスレッドのみである
  • 全てのサブクラスに対して契約的に成り立つ

notifyAllを使うことによって(superクラスの)notifyが機能しなくなる可能性がある。

notifyAllの用例
class PrintQueue {
    private SingleLinkQueue<PrintJob> queue = new SingleLinkQueue<PrintJob>();

    /**
     * プリントキューの追加<br>
     * 他の待っているスレッドに通知する
     */
    public synchronized void add(PrintJob job) {
        queue.add(job);
        notifyAll();
    }
    
    /**
     * 他のスレッドが何か挿入するまで待つ
     */
    public synchronized PrintJob remove() throws InterruptedException {
        while(queue.size() == 0) {
            wait();// プリントジョブが追加されるのを待つ
        }
        return queue.remove();
    }
}

各メソッドについて

wait

currentスレッドは以下の4つのケースまで待つ。

  • notifyがオブジェクトに対してcallされcurrentスレッドが実行可能になるまで
  • notifyAllがオブジェクトに対して呼ばれるまで
  • 指定されたtimeout時間まで
  • スレッドに対してinterruptメソッドがcallされるまで
public final void wait(long timeout) throws InterruptedException
上述の4つの条件までwaitする
public final void wait(long timeout, int nanos) throwa InterruptException
timeoutの時間がtimeout(ミリ秒) + nanos(ナノ秒)で指定できる
public final void wait()
wait(0)と同様

タイムアウトか条件を満たしたか知る必要がある場合は経過時間を計測する必要がある。

通知
public final void notifyAll()
条件の変化を待っている全てのスレッドに対して通知する
public final void notify()
条件の変化を待っている1津のスレッドに対して通知する。対象スレッドを選択することはできないので、待っているスレッドが単数に確実に絞れる場合にだけ使用する

ロックが保持されていない時に使用するとIllegalMonitorStateExceptionが投げられる。

■スケジューリング

実行しているスレッドは以下の条件のどれかまで実行を続ける。

  • wait
  • sleep
  • I/O
  • (タイムスライスなど)プリエンプションされるまで
  • VM依存
  • 共有リソースへのアクセスは常に保護する必要がある
  • 全ての順序はシステムに依存し特定の順序を期待するコードを書いてはいけない

各メソッドについて

public static void sleep(long millis) throws InterruptedException
現在実行中のスレッドを指定された秒数(以上)の間スリープさせる。スレッドがスリープ中に割り込まれるとIntterruptedExceptionが発生する
public static void sleep(long millis, int nanos) throws InterruptedException
millis + nanos秒の間(以上)の間スリープさせる。
public static void yield()
currentスレッドを実行させる必要がない事をスケジューラに知らせる。

スレッドの優先順位について

public final void setPriority(int newPriority)
Thread.MIN_PRIORITY〜Thread.MAX_PRIORITYで設定する
public final int getPriority()
スレッドの優先順位を返す

UIスレッドはNORM_PRIORITY+1で実行され、他のスレッドはNORM_PRIORITY-1で実行されることが多い。

■デッドロック

ランタイムはデッドロックを検出したり防いだりはしないので、設計でデッドロックの可能性を熟考する必要がある。

リソース順序付け

ロックを獲得するオブジェクトに関して常に同じ順序でロックを取得するようにする。

■スレッドの実行の終了

以下の条件の時にスレッドは終了する。

  • runメソッドが正常に戻る
  • runメソッドが突然完了する(例外が発生しキャッチされない場合など)
  • アプリケーションが終了する

スレッドのキャンセル

キャンセルは割り込みによって実現される。

thread2.interrupt();

thread2では以下のような処理が記述されているとする。

void tick(int count, long pauseTime) {
    try {
        for(int i = 0; i < count; i++) {
            System.out.println(',');
            System.out.flush();
            Thread.sleep(PauseTime);
        }
    }
    catch(InterruptedException e) {// 他のスレッドから割り込みされた時
        Thread.currentThread().interrupt();
    }
}

下述の2点に注意して強調しているコードがマルチスレッドを生かして処理できるようにする必要がある。

  • 割り込みを隠蔽しない
  • InterruptedExceptionをキャッチして正常に処理を進めようとしない

■スレッドの完了を待つ

class CalcThread extends Thread {
    private double result;
    @Override
    publc void run() {
        result = calculate();
    }
    publc double getResult() {
        return result;
    }
    publc double calculate() {
        // calc
    }
}
class ShowJoin {
    public static void main(String args[]) {
        CalcThread calc = new CalcThread();
        calc.start();
        doSomethingElse();
        try {
            calc.join();// joinが戻ってきたときにはrunが終了されていることが保証される
            System.out.println("result is " + calc.getResult());
        }
        catch(InterruptedException e) {
            System.out.println("No answer: interrupted");
        }
    }
}

各メソッドについて

public final void join(long millis) throws InterruptedException
スレッドが終了するか指定した時間が経過するまで待つ
public final void join(long millis, int nanos) throws InterruptedException
スレッドが終了するか指定した時間(millis + nanos)が経過するまで待つ
public final void join() throws InterruptedException
join(0)と同様

■アプリケーション実行を終了させる

スレッドの種類

  • user
  • demon

最後のユーザスレッドが終了するとデーモンスレッドは消滅させられてアプリケーションは終了する。

■メモリモデルとvolatile

同期順序は常にプログラム順序と整合性が取れる。単一のスレッドが更新し多数のスレッドから読み取る場合は同期の必要がないが、多数のスレッドが更新した値にアクセスするような場合は同期が必要でvolatile修飾子を検討する。

各メソッドについて

public Thread(ThreadGroup group, String name)
セキュリティ上可能なスレッドグループ内で与えられた名前を持つスレッドを生成する
public ThreadGroup(String name)
currentスレッドのグループを親グループとする新たなスレッドグループを生成する
public ThreadGroup(ThreadGroup parent, String name)
親グループを指定できる
public final String getName()
ThreadGroupの名前を返す
public final ThreadGroup getParent()
親のスレッドグループを返す
public final void setDaemon(boolean daemon)
カレントスレッドグループにデーモンステータスを設定する
public final boolean isDaemon()
カレントスレッドグループのデーモンステータスを返す
public final void setMaxPriority(int maxPri)
カレントスレッドグループの最大優先順位を設定する
public final int getMaxPriority()
カレントスレッドグループの最大優先順位を取得する
public final boolean parentOf(ThreadGroup group)
カレントスレッドグループがgroupの親であるか判定する
public final void checkAccess()
カレントスレッドがグループの修正を許されているか判定する
public final void destroy()
カレントスレッドグループを破棄する。スレッドが含まれていない事が前提で含まれていた場合はIllegalThreadStateExceptionを投げる
public int activeCount()
全てのサブグループのアクティブなスレッドを概算する
public int enumerate(Thread[] threadsInGroup, boolean recurse)
グループ内の全てのアクティブなスレッドへの参照を返す
public int enumerate(Thread[] threadsInGroup)
グループ内の全てのアクティブなスレッドへの参照を返す(再帰的)
public int activeGroupCount()
全てのサブグループのグループ数をカウントする
public int enumerate(ThreadGroup[] groupInGroup, boolean recurse)
グループ内の全てのアクティブなスレッドグループへの参照を返す
public int enumerate(ThreadGroup[] groupInGroup)
グループ内の全てのアクティブなスレッドグループへの参照を返す(再帰的)
public static int activeCount()
カレントスレッドのThreadGroup内のアクティブなスレッドの数を返す
public static int enumerate(Thread[] threadsInGroup)
グループ内の全てのアクティブなスレッドへの参照を返す
public void uncaughtException(Thread thread, Throwable exception)
threadがthrowされたexceptionにより終了するときに呼び出される

■デバッグ

public String toString()
スレッド名、優先順位、スレッドグループ名の文字列を返す
public Thread.State getState()
スレッドの現在の状態を返す{NEW, RUNNABLE, BLOCKED, WATING, TIMED_WATING, TERMINATED}
public static void dumpStack()
カレントスレッドのスタックトレースをSystem.errへ表示する
public String toString()
ThreadGroupの文字列表現を返す
public void list()
ThreadGroupを再帰的にリストしてSystem.outに表示する

コメントはまだありません»

No comments yet.

RSS feed for comments on this post.TrackBack URL

Leave a comment