↓前回記事
非同期デザインパターンシリーズの第2弾です。前回は「Single Threaded Executionパターン」を紹介しました。
この本の第2章「Immutableパターン」を基にしています。
##Immutableパターンとは?
前回記事の「Single Threaded Executionパターン」では、複数スレッドから使われた際に想定の動作をしないクラス/メソッドに対して、排他制御を行うことで想定の動きを保証するという考え方をしました。
今回はそもそも排他制御を行わなくてもよいようにするというImmutableパターンの思想でクラス設計を行います。
"Immutable"とは"変化しない"の意味で、クラスがマルチスレッドで稼働しても変化しない状態を表します。
##Immutableなクラスである条件
こうなっていれば必ずImmutableだ、という条件はないと思っています。が、最低限チェックが必要なのは下記でしょうか。
- そのクラスにクラス変数を変更するメソッド(setterメソッド)はないこと
- クラスが継承された際に、クラス変数を変更するメソッドが追加される可能性がないこと
- クラス変数が参照するインスタンスの状態が変更される可能性がないこと
個人的には、これらをきっちり担保するのは難しく、クラスが真にImmutableになるよう設計できるのはレアケースなんじゃないかと思ってます。
##具体例
####クラス設計
名前 | 役割 |
---|---|
Person | 人を表すクラス |
Main | 動作テストクラス |
PrintPersonThread | Personのインスタンス情報を表示するスレッドを表すクラス |
####実装
※こちら↓からDL可能です。
※筆者が作ったものではありません
https://www.hyuki.com/dp/dp2.html#download
public class Main {
public static void main(String[] args) {
Person alice = new Person("Alice", "Alaska");
new PrintPersonThread(alice).start();
new PrintPersonThread(alice).start();
new PrintPersonThread(alice).start();
}
}
public class PrintPersonThread extends Thread {
private Person person;
public PrintPersonThread(Person person) {
this.person = person;
}
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " prints " + person);
}
}
}
public final class Person {
private final String name;
private final String address;
public Person(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public String getAddress() {
return address;
}
public String toString() {
return "[ Person: name = " + name + ", address = " + address + " ]";
}
}
特にPersonクラスの作りについて、下記の特徴があります。
-
final
クラスとなっており、継承を禁止している - クラス変数(name,address)が
private
となっており、Personクラスからのみ使用可能 - クラス変数(name,address)が
final
となっており、setter
メソッドがない。これにより、コンストラクタ生成時以外で代入が不可能
####動作確認
※一部抜粋
Thread-1 prints [ Person: name = Alice, address = Alaska ]
Thread-1 prints [ Person: name = Alice, address = Alaska ]
Thread-0 prints [ Person: name = Alice, address = Alaska ]
Thread-0 prints [ Person: name = Alice, address = Alaska ]
Thread-0 prints [ Person: name = Alice, address = Alaska ]
Thread-0 prints [ Person: name = Alice, address = Alaska ]
Thread-2 prints [ Person: name = Alice, address = Alaska ]
Thread-2 prints [ Person: name = Alice, address = Alaska ]
Thread-2 prints [ Person: name = Alice, address = Alaska ]
Thread-2 prints [ Person: name = Alice, address = Alaska ]
Thread-2 prints [ Person: name = Alice, address = Alaska ]
Thread-0 prints [ Person: name = Alice, address = Alaska ]
複数スレッドからtoStringメソッドが呼び出されていますが、期待通りにPersonの情報を出力し続けています。
toStringをsyncronized
メソッドにしなくても、期待通りに動いています。
これはPersonクラスが一度作られた後にフィールドの値を変更できない、つまりImmutableであるからです。
ImmutableっぽいけどImmutableじゃないパターン
setterメソッドもないし、クラス変数もfinalにしてコンストラクタからしか変更できないようにしたけど..
public final class UserInfo {
private final StringBuffer info;
public UserInfo(String name, String address) {
this.info = new StringBuffer("****");
}
public StringBuffer getInfo() {
return info;
}
public String toString() {
return "[ UserInfo: " + info * " ]";
}
}
結論からいうと、このクラスはImmutableではありません。下記のように、クラス変数がfinalであっても、変数の参照先インスタンスの状態がImmutableでなければ、複数スレッドから変更される可能性があります。
//変数infoの参照先インスタンスを取得
StringBuffer buffer = getInfo();
//文字順を逆にする
buffer.reverse();
PersonクラスがImmutableであったのは、java.lang.StringクラスがImmutableなクラスだったからです。これに対しStringBuffer,StringBuilderなどのクラスは自らの状態を変えるメソッドを持っているため、可変です。
上記のUserInfoクラスはは冒頭で述べたチェック項目
- クラス変数が参照するインスタンスの状態が変更される可能性がないこと
に違反しているため、Immutableではなくなっていたのでした。