RMIのサーバーオブジェクトはスレッドセーフでなくてはならない
RMIのAPIのJava Docに書かれていないようなので見落としがちなことですが、RMIのサーバーオブジェクト(Remoteの実装クラス)は、複数のスレッドから同時に呼び出される(可能性がある)ようです。このことは、
Java並行処理プログラミング ―その「基盤」と「最新API」を究める―
- 作者: Brian Goetz,Joshua Bloch,Doug Lea
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2006/11/22
- メディア: 単行本
- 購入: 30人 クリック: 442回
- この商品を含むブログ (174件) を見る
- 作者: Brian Goetz,Tim Peierls,Joshua Bloch,Joseph Bowbeer,David Holmes,Doug Lea
- 出版社/メーカー: Addison-Wesley Professional
- 発売日: 2006/05/09
- メディア: ペーパーバック
- 購入: 7人 クリック: 14回
- この商品を含むブログ (22件) を見る
http://download.oracle.com/javase/1.4.2/docs/guide/rmi/spec/rmi-arch3.html
RMIのサーバーオブジェクトはスレッドセーフなクラスとして実装しなくてはならないと書かれています。RMIの仕様ではサーバーオブジェクトが実際にマルチスレッドで呼び出されるかどうかは規定しておらず、実装依存としていますが、普通に考えたら逐次処理では遅すぎて使いものになりませんので。(SunのRMIの実装が、実際にどうなっているかは未調査)
それゆえ、先日Java言語のチェック例外は本当にGood Partなのか? - 達人プログラマーを目指してで紹介したJava: The Good Partsの9章のサンプルコードを正しく理解する際には注意が必要です。残念ながら、この本ではマルチスレッドは後の10章で説明するということもあるかもしれませんが、RMIを実装する際のスレッドセーフに関する注意点について正しく言及されていません。
実際には9章のサンプルコードではHashMapではなくHashtableを利用しているため、最初スレッドセーフ性を考慮しているのかと思いましたが、ダウンロードしたサンプルコードを読む限り、スレッドセーフな実装にはなっていませんでした。この本を読む場合には、この点十分注意して読む必要があると思います。実際、この本の10章の記述を読むと分かりますが、synchronizedを使ったスレッドプログラミングに関して原著者の誤解があるように思われます。finalやvolatileの適切な使用もされておらず、コンストラクタ内で生成途中のオブジェクトを公開しているところなどもお手本としてはいただけませんね。スレッドプログラミングでは原子性と共に可視性という考え方を理解する必要があります。これについては、以下が参考になります。
並行処理におけるメモリの可視性保証について - じゅんいち☆かとうの技術日誌
マルチコア時代に備えて本気でメモリモデルを理解しておこう - リオーダー & finalフィールド 編 - - じゅんいち☆かとうの技術日誌
Java Good Partsサンプルより引用 package org.oreilly.javaGoodParts.examples.impl; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; import java.util.Hashtable; import java.util.List; import java.util.Set; import java.util.UUID; import org.oreilly.javaGoodParts.examples.statistics.BoxScore; import org.oreilly.javaGoodParts.examples.statistics.Player; import org.oreilly.javaGoodParts.examples.statistics.StatRecorder; import org.oreilly.javaGoodParts.examples.statistics.Team; /** * An implementation of the StatRecorder interface. This will * create a server that is exported using the default RMI registry * (which will need to be started by some other means) on the * standard port (1099). The server will name itself Recorder, * and can be found by clients if they know the machine on which * the server is running. */ public class StatRecorderImpl implements StatRecorder { private Hashtable<String, Team> teams = new Hashtable<String, Team>(); private Registry registry; private StatRecorder myStub; StatRecorderImpl(List<Team> initTeams) { for (Team t : initTeams) { teams.put(t.getName(), t); } try { exportRecorder(); } catch (RemoteException e) { System.out.println("unable to export stat recorder"); } } @Override public void recordGame(BoxScore stats) throws RemoteException { for (String teamName : stats.getTeams()) { Team toUpdate = teams.get(teamName); processScore(toUpdate, stats); } } @Override public Set<Player> getRoster(String forTeam) throws RemoteException { return (teams.get(forTeam)).getRoster(); } /** * Export a stub object so that calls can be made from * another address space thorugh that object. This is done * by putting the stub in a {@link Registry}, which itself * is a remote object that others can use to find the stub * that in turn is used to call the remote objects of this * implementation * @throws RemoteException */ private void exportRecorder() throws RemoteException { if (System.getSecurityManager() == null){ System.setSecurityManager(new SecurityManager()); } registry = LocateRegistry.getRegistry(); myStub = (StatRecorder) UnicastRemoteObject.exportObject(this, 5550); registry.rebind("Recorder", myStub); } /** * Process the box score for a particular team. This * implementation will go through the players (by their * id), and call {@link upDatePlayer} for each player * that was in the game * @param forTeam the team whose players are being * updated * @param game the {@link BoxScore} object that * contains the record of the game */ private void processScore(Team forTeam, BoxScore game) { List<UUID> players = game.getPlayers(forTeam.getName()); for (UUID id : players) { Player toUpdate = forTeam.getPlayer(id); upDatePlayer(toUpdate, game); } } /** * Update the statistics of a particular player, given * the boxscore of the game. The actual implementation * of this method is an exercise left to the reader... * @param toUpdate * @param game */ private void upDatePlayer(Player toUpdate, BoxScore game) { } }
JDK1.2の頃はsynchronziedによる同期化は非常に遅いと思われていましたし、なるべく同期ブロックを少なく、かつ、同期のスコープを狭くするということがベストプラクティスと考えられていました。ただし、安易にそのような設計をすると複数オブジェクト間の異なるロックの取得順序でデッドロックが起こったり、結局手に負えない設計となってしまいます。当時はJavaのマルチスレッドプログラミングのデザインパターンはあまり知られていませんでした。一応この本の原著者の名誉のために断っておくと、上記でupDatePlayer()メソッドは読者の宿題ということになっているので、このメソッドの実装をすると同時に同期化も考えましょうということなのかなと思います。
このクラスの場合、最も単純にスレッドセーフ化するには、可変(ミュータブル)な状態にアクセスする(更新だけでなく参照も)すべてのメソッドをsynchronizedにしてしまうのがとりあえずの解決策となります。同期化を外側で行ってしまい、内部で使用されるオブジェクトが複数のスレッドから呼び出されないようにすることで、内部のオブジェクトは一応スレッドセーフ性を考慮せずにプログラミングできます。(PlayerやTeamは可変な設計となっているため、複数スレッドから呼び出す場合はすべて同期化が必要になってしまう。)
この設計の欠点は並列性が妨げられることにより、同時に多数呼び出された場合の性能が頭打ちになると考えられることです。一つの解決策としては、Read Writeロックパターン*1というのが知られていますが、Java SE5からは、以下のクラスが標準でサポートされています。
http://java.sun.com/javase/ja/6/docs/ja/api/java/util/concurrent/locks/ReentrantReadWriteLock.html
このパターンを使うと状態の読み取りしか行わないメソッド同士はReadロックの取得のみで済む為、同時に実行することが可能になります。ただし、JavaSEの、このAPIを直接使った場合、不注意でロックの解放漏れなどが起こり得るためAOPを組み合わせるなどの対処が必要です。
なお、Java EE6からはシングルトンEJBというのが利用できるのですが*2、この機能を使うと以下のような感じでロックの取得と解放を自動的に行わせることができます。
→例題のプログラムをシングルトンEJBとして実装 @Singleton public class StatRecorderImpl implements StatRecorder { @ConcurrencyAttribute(READ_WRITE_LOCK) public void recordGame(BoxScore stats) throws RemoteException { for (String teamName : stats.getTeams()) { Team toUpdate = teams.get(teamName); processScore(toUpdate, stats); } } @Override @ConcurrencyAttribute(READ_LOCK) public Set<Player> getRoster(String forTeam) throws RemoteException { return (teams.get(forTeam)).getRoster(); } ... }
あとは、同期化の不要な不変な(イミュータブル)なクラスに設計しなおすとか、Scalaのような関数型のパラダイムを持った言語を使い、できるだけ可変な変数の使用を避けるなどが可能でしょうか。このRMIサーバーの実装をScalaで書き直してみると関数型のよさが分かるのかなと思います。同時にJavaのBad Partが露呈してしまうことにもなるのですが、Java 5以降では、ConcurrentHashMapなどロックではなくコピーによって整合性を保つような仕組みのクラスがいろいろと利用できますし、Google CollectionsやAkkaなどの新しいライブラリーも活用すれば、並列化に関しては多少関数型に近いモデルで実装可能な余地はあるかもしれません。