Javaでマルチスレッドプログラミング -排他制御-

Javaでマルチスレッドプログラミングを覚えようと、こちらのサイトを読んで写経してみた。前回の記事では、マルチスレッドの概要と実行方法をまとめた。今回はマルチスレッドの排他制御について。
 
複数のスレッドが同じオブジェクトを同時に操作すると、プログラムが予想外の動作をすることがある。この記事では、複数のスレッドの動作を制御し、同じオブジェクトが同時に操作されないようにする方法を説明する。
 

複数のスレッドが並行して単一のインスタンスを操作することの問題点

2つのスレッドは、同じオブジェクトを並行して同時に扱うことができる。2つのスレッドがあるオブジェクトのフィールド変数を同時に書き込んだりすると、プログラムが時として意図しない動作をすることがある。
 

class Account {
    private int balance = 0;         // 預金残高
    public void deposit(int money){
        int total = balance + money;
        balance = total;
    }
}

 
上記の預金口座を表すクラスでは、同じインスタンスに対して2つのスレッドが同時にアクセスした場合、問題が生じる恐れがある。
例えば、以下のような順序で処理が行われた場合、プログラムが意図していない結果となる。
 
スレッドAとスレッドBが同じインスタンスのdeposit(1000)を呼び出した場合、

  1. スレッドAがint total = balance + money;を実行する
  2. スレッドBがint total = balance + money;を実行する
  3. スレッドAがbalance = total;を実行する
  4. スレッドBがbalance = total;を実行する

 
意図としては、balance変数が2000になっていることを期待しているが、実際にはbalance変数は1000になってしまっている。
 
※totalはメソッドで定義されたローカル変数なので、メソッドの呼び出しごとに個別に領域確保される。つまり、それぞれのスレッドで別々の領域を使用している(この辺の挙動はJVMの仕様を参照してほしい)。
 
※balanceはインスタンス変数なので、スレッド1とスレッド2で領域を共有している。
 

スレッド固有の作業コピー領域

上記の内容の続きだが、以下のように複数行に分けて書いていたプログラムを1行で書いてしまえば、処理の重複を避けることが出来そうに見える。

public void diposit(int money){
    balance += money;
}

 
しかし、これでも誤動作の可能性がある。それぞれのスレッドは内部的に固有の作業コピー領域を持っており、より高速にプログラムを実行するために、変数の値を一時的にスレッド固有のメモリにコピーして作業することが許されている。上記の例では、balanceの値を共有メモリからスレッド固有メモリにコピーし、そこで加算処理を実施してから共有メモリに書き戻す、という処理を行う。つまり、このように1行でプログラムを書いたとしても、先ほどのプログラムと同じようなことが内部的に実行されてしまう。
 
作業コピー領域で起きていることを図に表すと以下のようになる。
※図はこちらのサイトから引用させていただいた。
ThreadWorkingCopy
 

synchronizedブロック

上記で扱ってきた問題を解決するにはsynchronizedブロックを利用する。
クラスやインスタンスはそれぞれ「ロック」というものを持っている。ロックとは「鍵」のことである。スレッドは、synchronizedブロックの開始時にsynchronized文で指定したオブジェクトのロックを取得し、終了時にそのロックを開放する。あるスレッドがロックを取得している状態では、他のスレッドは同じロックを取得することができず、ロックが返却されるまで待たされることになる。
ただし、ロックが使用中であるかどうかを気にするのは、あくまでも、スレッドがsynchronizedブロックを実行しようとしているときだけである。あるスレッドがsynchronizedブロックでオブジェクトを独占しているつもりでも、synchronizedを利用していない箇所からのアクセスを防ぐことはできない。

public void deposit(int money) {
    synchronized(this){
        int total = balance + money;
        balance = total;
    }
}

  

synchronizedメソッド

メソッド全体をオブジェクトthisに対するsynchronizedブロックとしたい場合、synchronizedメソッドを利用することができる。synchronizedメソッドは、メソッドの処理全体を、thisに対するsynchronizedブロックで囲んだのと同じ意味を持つ。

synchronized public void deposit(int money) {
    int total = balance + money;
    balance = total;
}

 
上記のような単純な処理でsychronizedメソッドを使う分には問題ない。しかし、長いメソッドや処理に時間のかかるメソッドでは無闇にsynchronizedメソッドを使うべきではない。他のスレッドがロック解放待ちのために長時間待たされるおそれがあるので、排他制御は、誤動作が起きない必要最低限の狭い範囲に限定して使用するべきである。
 

static synchronizedメソッド

クラス(static)メソッドもsynchronizedメソッドにすることができる。クラスメソッドはそのクラスのインスタンスが存在していなくても呼び出すことができるが、スレッドはどのオブジェクトのロックを取得するのか。
 
あるクラスがプログラムにおいて利用可能であるとき、必ずそのクラスに対応するjava.lang.Classクラスのオブジェクトが存在している。スレッドは、static synchronizedメソッドを実行するとき、そのクラスに対応するClassオブジェクトのロックを取得する。
なお、クラスに対応するClassオブジェクトは、ClassクラスのクラスメソッドであるforNameメソッドを用いて取得することができる。
 
つまり、以下の2つのコードは同じ意味になる。

class SomeClass {
    synchronized static public void someMethod() {
        //...
    }
}

 

class SomeClass {
    static public void someMethod() {
        synchronized(Class.forName("SomeClass")) {
            //...
        }
    }
}

 

volatile変数

「スレッド固有の作業コピー領域」の説明のところにも書いたように、各スレッドは、共有する変数の内容をスレッド固有の作業領域にコピーして作業を行う。したがって、共有する変数を使用するためには、作業コピーへの読み込みや、共有メモリ(変数)への書き込みを行う必要がある。この読み書きのタイミングにはある程度の自由が許されており、連続して同じ変数にアクセスするときには途中で書き戻さない(作業コピーを変数に反映しない)ことや、プログラムで記述した順序とは違う順序で書き戻すことが許されている。
 
次のプログラムのincrementメソッドでは、まずaを加算してからbを加算するように記述している。しかし、あるスレッドでincrementメソッド実行中に別スレッドからprintメソッドを実行した場合、「a=0,b=1」と表示される可能性がある。

class someClass {
    private int a = 0, b = 0;
    public void increment() {
        a++;
        b++;
    }
    public void print(){
        System.out.println("a=" + a + ",b=" + b);
    }
}

 
そのような意図しない動作を防ぐためには、共有される変数をvolatileとして宣言する。volatile変数は、スレッドからアクセスがあるたびに、必ず共有メモリ上の変数の値とスレッドの作業コピー上の値とを一致させる。

volatile private int a = 0, b = 0;

 
無用のトラブルを防ぐには、複数のスレッドからアクセスされる可能性のあるメンバ変数はvolatileとして宣言しておくのがよい。ただし、synchronizedブロックでのロックの開放時には、必ず作業コピーの内容は共有メモリに書き戻されるので、synchronizedブロック内からしかアクセスされない変数はvolatile変数にする必要はない。
 
 
以上
 
 
参考
マルチスレッドプログラミング | TECHSCORE(テックスコア)

Article written by

Comments are closed, but trackbacks and pingbacks are open.