[PR] 迷惑メール トップページプロ3ゼミJavaJava入門講座三学期単位13 →4時間目

4時間目 排他制御で制御しよう


13−4−1.マルチスレッドの難しさ

 マルチスレッドを行えば楽に処理速度が向上するようにみえるかもしれませんが、マルチスレッドの扱いには 気をつけなければなりません。
例えば、預金を預けたり引き出すと言う事は銀行に預けている預金額を足したり引いたりしている事ですよね。
次のような事が実際に起こったら大変な事態になってしまいます。
銀行に預けてある金額を10万円としましょう。
Aが預金額を取得(10万円)
Bが預金額を取得(10万円)
Aが1万円預ける(10万円 + 1万円で11万円)
Bが2万円引き出す(10万円 - 2万円 = 8万円)
どういうことか分かったでしょうか。
本来なら預金額が9万円のはずが8万円になっています。
問題になっているのはBが取得した10万円から2万円を引いている事が原因です。
預金などのように共有するデータには、排他制御と同期制御を行い制御する必要があります。
排他制御とは、同時にアクセスされない(排他)ように制御することです。
同期制御とは、スレッド同士で同期を取る事により制御することです。
BankCommon.java
package first.java.unit13.model;

// 銀行に関するインタフェース
public interface BankCommon {

    // お金を預金額に追加する
    public void add(long money);

    // お金を預金額を引き出す
    public long get(long money);
}

Bank.java
package first.java.unit13.model;

// 銀行
public class Bank implements BankCommon {

    // 預金額
    private long    saving;

    // 最初の預金金額
    public Bank() {

        saving = 100000;
        System.out.println("最初の預金額は" + saving + "円あります");
    }

    // お金を預金額に追加する
    public void add(long money) {

        long now = saving + money;

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        saving = now;
        String name = Thread.currentThread().getName();
        System.out.println(name + ":預金額 = " + saving);
    }

    // お金を預金額を引き出す
    public long get(long money) {

        long now = saving - money;

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        saving = now;
        String name = Thread.currentThread().getName();
        System.out.println(name + ":預金額 = " + saving);
        return saving;
    }
}

AUser.java
package first.java.unit13.model;

// Aユーザ
public class AUser implements Runnable {

    // 使用する銀行
    private BankCommon  bank;

    // 使用する銀行を設定
    public AUser(BankCommon bank) {

        this.bank = bank;
    }

    // スレッドはここからスタート
    public void run() {

        bank.add(10000);
    }
}

BUser.java
package first.java.unit13.model;

// Bユーザ
public class BUser implements Runnable {

    // 使用する銀行
    private BankCommon  bank;

    // 使用する銀行を設定
    public BUser(BankCommon bank) {

        this.bank = bank;
    }

    // スレッドはここからスタート
    public void run() {

        bank.get(20000);
    }
}

Sample13_4_1.java
package first.java.unit13.sample;

import first.java.unit13.model.AUser;
import first.java.unit13.model.BUser;
import first.java.unit13.model.Bank;

// 単位13 4時間目 サンプル1
public class Sample13_4_1 {

    // ここからスタート
    public static void main(String[] args) {

        Bank bank = new Bank();

        Runnable aUser = new AUser(bank);
        Runnable bUser = new BUser(bank);

        Thread addThread = new Thread(aUser, "Aユーザ");
        Thread getThread = new Thread(bUser, "Bユーザ");

        addThread.start();
        getThread.start();
    }

}
最初の預金額は100000円あります
Aユーザ:預金額 = 110000
Bユーザ:預金額 = 80000
    

 例えのAユーザとBユーザを再現する為に、預金の預けると引き出しの処理中に「Thread.sleep()」メソッドを 使いましたが、実際にもAユーザが預金を取得後にBユーザが預金を取得しないとは言えません(サンプルはで、 確率を上げる為にスリープを使用しました)。

13−4−2.排他制御と同期制御のJavaの実装法

 マルチスレッドには排他制御と同期制御で制御する事ができると説明しましたが、Javaではどのように実現す るかをみていきます。

 排他制御では同時にアクセスされないようにするのですから、メソッドにロックする事により実現しています 。
メソッドに「synchronized」修飾子を指定することにより、ロックされメソッドの実行が終了するまで他のアク セスを待ち状態にします。
synchronized修飾子の構文は、
アクセス修飾子 synchronized 戻り型 メソッド名(引数) {
    :
    :
}
の通りです。
又、ブロックの指定も出来ます。
ブロックは、
synchronized(オブジェクト) {
    :
    :
}
とメソッド内に書けば「synchronized」ブロックの範囲だけロックされます。
オブジェクトの指定で例えば「this」を指定しメソッド全体を囲めばメソッドに「synchronized」修飾子を指定 したのと同じ効果です。

 同期制御の方法は、「Object」クラスの「wait()」メソッドで待ち状態にし、「Object」クラスの「notify() 」メソッド、「notifyAll()」メソッドで解除します。

●wait()メソッド
 「wait()」メソッドを呼び出したスレッドは「notify()」メソッド、「notifyAll()」メソッドを呼び出され るまでは待ち状態になります。
●notify()メソッド
 wait()メソッドで待ち状態になった1スレッドを待ち状態から解除します。
●notifyAll()メソッド
 wait()メソッドで待ち状態になったすべてのスレッドを待ち状態から解除します。

13−4−3.排他制御を試してみよう

 AユーザとBユーザの問題は排他制御で解決する事ができます。
プログラムを変更してあるので見てみましょう。
BankControl.java
package first.java.unit13.model;

// 銀行排他制御と同期制御済み
public class BankControl implements BankCommon {

    // 預金額
    private long    saving;

    // 最初の預金金額
    public BankControl() {

        saving = 100000;
        System.out.println("最初の預金額は" + saving + "円あります");
    }

    // お金を預金額に追加する
    public synchronized void add(long money) {

        long now = saving + money;

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        saving = now;
        String name = Thread.currentThread().getName();
        System.out.println(name + ":預金額 = " + saving);
    }

    // お金を預金額を引き出す
    public synchronized long get(long money) {

        long now = saving - money;

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        saving = now;
        String name = Thread.currentThread().getName();
        System.out.println(name + ":預金額 = " + saving);

        return saving;
    }
}

Sample13_4_2.java
package first.java.unit13.sample;

import first.java.unit13.model.AUser;
import first.java.unit13.model.BUser;
import first.java.unit13.model.BankControl;

// 単位13 4時間目 サンプル1
public class Sample13_4_2 {

    // ここからスタート
    public static void main(String[] args) {

        BankControl bank = new BankControl();

        Runnable aUser = new AUser(bank);
        Runnable bUser = new BUser(bank);

        Thread addThread = new Thread(aUser, "Aユーザ");
        Thread getThread = new Thread(bUser, "Bユーザ");

        addThread.start();
        getThread.start();
    }

}
最初の預金額は100000円あります
Aユーザ:預金額 = 110000
Bユーザ:預金額 = 90000

 「synchronized」メソッドは、そこで使用しているフィールド変数の取得を制御している為、適切に処理され ている事が分かります。
預金の預け入れと引き出しのスリープ時間を変えてみるなど色々と試してみて適切に処理されるかを確かめてみ ると排他制御の安全性がわかると思います。

3時間目 スレッドの状態を理解するに戻る     単位14 入出力処理と長いソース

トップページに戻る