Javaに関する様々な情報をご紹介します。

4スレッドの同期

Javaに関する様々な情報をご紹介します。

スレッドの同期

スレッド処理を行う場合、複数のスレッドで1つの値を共有したい場合があります。またその値を意図したように操作するために、複数のスレッドの動作を制御したい場合があります。このページでは、それらスレッドの動作を制御する方法について説明します。

概要

例題として、配列priceに設定された課税前の価格を一つづつ抜き出し変数workAreaに代入し、workAreaに代入された価格に消費税を課税する処理を行います。課税前の価格を抜き出しworkAreaに代入する処理をpricePutメソッドにて、workAreaに代入された価格に課税する処理をpriceGetメソッドにて行います。

スレッドの同期(例題)

この処理を行う上でスレッドの実行順序を制御せずに実行した場合は、スレッドの実行順序はJavaの動作するシステムのスケジュール機能に依存するため、必ずしもpricePutメソッドとpriceGetメソッドが交互に実行されるとは限りません。その場合、実行結果は以下のようになる場合があります。

課税後価格は105.0円です。
課税後価格は210.0円です。 # pricePutで新しい価格が設定される前に
課税後価格は210.0円です。 # priceGetが実行されたため価格がダブっている。
課税後価格は420.0円です。
課税後価格は525.0円です。

実行結果を意図したように導き出すためには以下の点を考慮する必要があります。

  1. pricePutメソッド、もしくはpriceGetメソッドでオブジェクトの値(ここでは変数workArea)を操作している間、他の処理からオブジェクトへのアクセスを防ぐ必要があります。pricePutメソッドで値を設定していると同時に、priceGetメソッドでオブジェクトへアクセスがあった場合、そのオブジェクトが破壊されてしまう恐れがあります。これを防ぐためにはsynchronizedを使用します。synchronizedで指定された処理は、処理を実行しているオブジェクトを他の処理からアクセスされないようロックします。
  2. pricePutメソッドで変数workAreaに値を代入した後にpriceGetメソッドでその値を抜出す必要があります。このためにはpricePutメソッドを動作させるスレッドとpriceGetメソッドを動作させるスレッドの間で何らかの同期を行う必要があります。スレッドを同期させる方法として、waitメソッド、notifyAllメソッドを使用します。

synchronized

synchronizedはメソッド・ブロックに対して指定するものです。synchronizedが指定されると、指定されたメソッド・ブロックが属するオブジェクトはメソッド・ブロックを実行している間、同じオブジェクト内のsynchronizedが指定されたメソッド・ブロックからアクセスされないようロックされます。

オブジェクトを共有するメソッド・ブロックをクリティカルセクションと言います。synchronizedはクリティカルセクションに指定します。今回の例題では、変数workAreaを共有するpricePutメソッドと、priceGetメソッドがクリティカルセクションです。

メソッドに指定する場合

synchronized 戻り型 メソッド名(引数型 引数名){
  メソッド本体
} ;

ブロックに指定する場合

synchronized (ロックするオブジェクト){
  クリティカルセクションコード
} ;

pricePutメソッドにより変数workAreaに値をセットする間、同じオブジェクトに属するpriceGetメソッドは実行することができません。priceGetメソッドにより変数workAreaから値を抜出す間、同じオブジェクトに属するpricePutメソッドは実行することができません。

スレッド_synchronized

synchronized関連コード

・・・・・・・・・・

//synchronized指定。pricePutメソッドが終了するまで、
//メソッドが属するオブジェクトはアクセスされない
public synchronized void pricePut(int price) {
    while (available == true) {
        try {
            wait();
        } catch (InterruptedException e) {
        }
    }
    //workAreaに値をセットする処理
    workArea = price;
    available = true;
    notifyAll();
}

//synchronized指定。priceGetメソッドが終了するまで、
//メソッドが属するオブジェクトはアクセスされない
public synchronized void priceGet() {
    while (available == false) {
        try {
            wait();
        } catch (InterruptedException e) {
        }
    }
    //workAreaから値を抜出す処理
    System.out.println("課税後価格は" +
    workArea * 1.05 + "円です。");
    available = false;
    notifyAll();
}

・・・・・・・・・・

※synchronizedはオブジェクトに対してロックを行います。pricePutメソッドとpriceGetメソッドが同一のオブジェクトに属する場合は互いにロックを行います。pricePutメソッドとpriceGetメソッドが異なるオブジェクトに属する場合は、それぞれのオブジェクトによりそれぞれのメソッドが実行(変数workAreaにアクセス)されます。

wait、notifyAll

pricePutメソッドとpriceGetメソッドを同時にオブジェクトにアクセスしないようにした後はpricePutメソッドとpriceGetメソッドを交互に実行させるようにします。これにはwaitメソッド、notifyAllメソッドを使用します。

waitはそれまで保持していた実行権を解除し、他のスレッドに実行権を渡します。またその際、synchronizedによってロックされていたオブジェクトも他のスレッドからアクセスできるように解除されます。waitを呼び出したスレッドは他のスレッドからnotifyAll(もしくはnotify)が呼び出されるか、引数に指定した一定時間を経過するまで実行不可状態でありつづけます。

notifyAllはwaitにより実行不可状態にあるスレッドを実行可能状態にします。waitにより実行不可状態であるスレッドが複数ある場合はすべてのスレッドを実行可能状態にします。実行可能状態となったスレッドは優先度及び、Javaのよる独断的決定に従い実行状態となります。この他notifyというメソッドもあります。notifyは複数あるwait状態にあるスレッドのうち一つを実行可能状態にします。どのスレッドを実行可能状態にするか選択することはできません。

wait、notify、notifyAllはsynchronized指定がされたメソッド・ブロックでのみ使用できます。synchrnonized指定されていないメソッド・ブロックで使用した場合は例外IllegalMonitorStateExceptionが発生します。

notifyAll、wait関連コード

・・・・・・・・・・

public synchronized void pricePut(int price){
    while (available == true) {
        try {
            wait();  //availableがtrueの間、wait
        } catch (InterruptedException e) {
        }
    }
    workArea = price;
    available = true;
    //availableeにtrueを代入した後wait状態のスレッドを解除
    notifyAll();
}

public synchronized void priceGet() {
    while (available == false) {
        try {
            wait();  //availableがfalseの間、wait
        } catch (InterruptedException e) {
        }
    }
    System.out.println("課税後価格は" +
    workArea * 1.05 + "円です。");
    available = false;
    //availableにfalseを代入した後wait状態のスレッドを解除
    notifyAll();
}

・・・・・・・・・・

今回の例題ではboolean型変数availableと組み合わせて、pricePutメソッドとpriceGetメソッドを交互に実行させていきます。まずpricePutメソッドにおいて、available=tureの間(新しい課税前価格が設定されたが、priceGetメソッドによりその価格が抜出されていない状態)、waitを行うようにします。waitを行っている間、ロックが解除され、他のスレッドは実行権が渡されるためpriceGetメソッドが実行されます。priceGetはavailable=trueのため、whileループ以下の処理を行い、処理後availableにfalseを代入し、notifyAllを実行します。notifyAllによりpricePutのwait状態が解除された段階ではavailable=falseのためwhileループを抜けwhileループ以下の処理(新しい価格の設定)の実行を行います。

priceGetメソッドにおいても同様の実行手続きをとります。available=falseの場合(priceGetメソッドにより価格が抜出された後、一度も新しい課税前価格が設定されていない状態)、waitを実行し、実行権をpricePutに渡します。pricePutにおいて新しい価格が設定された後、notifyAllによりwait状態が解除されその新しい価格に対する課税処理が行われます。

スレッドの実行順序はJava実行システムが動作するシステムのスケジュール機能に依存するため、意図的に制御することができません。そのため、Java実行システムがどのスレッドを実行するかに関わらず、変数availableの値により実行順序を制御します。変数availableの値の変更はwaitメソッド・notifyAllメソッドの仕組みを使い、スレッド間で通知されます。

例題コード

以下に本例題で使用したコードを記載します。

CalculatePrice.javaファイル

class CalculatePrice {
    private int workArea;
    private boolean available = false;

    //synchronized指定。pricePutメソッドが終了するまで、
    //メソッドが属するオブジェクトはアクセスされない
    public synchronized void pricePut(int price) {
        while (available == true) {
            try {
                wait();  //availableがtrueの間、wait
            } catch (InterruptedException e) {
            }
        }
        //workAreaに値をセットする処理
        workArea = price;
        available = true;
        //availableにtrueを代入した後wait状態のスレッドを解除
        notifyAll();
    }

    //synchronized指定。priceGetメソッドが終了するまで、
    //メソッドが属するオブジェクトはアクセスされない
    public synchronized void priceGet() {
        while (available == false) {
            try {
                wait();  //availableがfalseの間、wait
            } catch (InterruptedException e) {
            }
        }
        //workAreaから値を抜出す処理
        System.out.println("課税後価格は" +
        workArea * 1.05 + "円です。");
        available = false;
        //availableにfalseを代入した後wait状態のスレッドを解除
        notifyAll();
    }
}

PutPrice.javaファイル

class PutPrice extends Thread {
    private CalculatePrice cp;
    private int[ ] price = {100, 200, 300, 400, 500};

    //コンストラクタ内で引数に指定されたオブジェクトcpを代入
    public PutPrice(CalculatePrice cp) {
        this.cp = cp;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            //オブジェクトcpのpricePutメソッドを実行
            cp.pricePut(price[i]);
            try {
                sleep((int)(Math.random() * 100));
            } catch (InterruptedException e) {
            }
        }
    }
}

GetPrice.javaファイル

class GetPrice extends Thread {
    private CalculatePrice cp;
    //コンストラクタ内で引数に指定されたオブジェクトcpを代入
    public GetPrice(CalculatePrice cp) {
        this.cp = cp;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            //オブジェクトcpのpriceGetメソッドを実行
            cp.priceGet();
        }
    }
}

ExThread5.javaファイル

public class ExThread5 {
    public static void main(String[] args) {
        CalculatePrice cp = new CalculatePrice();
        //オブジェクトcpを引数にスレッドオブジェクトppの生成
        PutPrice pp = new PutPrice(cp);
        //オブジェクトcpを引数にスレッドオブジェクトgpの生成
        GetPrice gp = new GetPrice(cp);

        pp.start();  //スレッドを実行可能状態にする
        gp.start();  //スレッドを実行可能状態にする
    }
}

実行結果

D:\JAVA>javac ExThread5.java

D:\JAVA>java ExThread5
課税後価格は105.0円です。
課税後価格は210.0円です。
課税後価格は315.0円です。
課税後価格は420.0円です。
課税後価格は525.0円です。

D:\JAVA>

4スレッドの同期