『増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編』に載っているデザインパターンを Groovy/GPars で書こうシリーズ、今回は Read-Write Lock パターン。 このパターンも Java SE 5 で導入された java.util.concurrent パッケージ(正確には java.util.concurrent.lock パッケージ)に定義されているクラス群を使うと簡単に実装できます。
今までのデザインパターンで処理を待たせる、つまりある意味でロックをとるものがいくつかありましたが、Read-Write Lock パターンでは、読み込みと書き込みで違ったロックの取り方をするのが特徴です。
- 複数のスレッドから並行に読み込みができる
- 単一のスレッドからしか書き込みができない
- 書き込んでいる最中は読み込みができない
といった具合です。 こういうロックの取り方は結構必要になることが多いようなので、標準 API としてこれらを実装したクラス群が提供されてるんでしょう。
java.util.concurrent に定義されている Read-Write Lock に関連するインターフェースは
- ReadWriteLock
- Lock
です。

ロックの取得や解放は Lock オブジェクトを用いて行いますが(Lock#lock(), Lock#unlock() メソッド)、これらの Lock オブジェクトは ReadWriteLock オブジェクトから生成もしくは取得します。 ReadWriteLock インターフェースを実装した具象クラスは、標準 API で ReentrantReadWriteLock クラスのみが提供されています。 ちなみに、Read-Write Lock ではなく、単純なロックを使いたい場合は Lock インターフェースとそれを実装した ReentrantLock クラスも使えます。
サンプル・コード
サンプルで作成するソースは
- Data クラス
- WriterActor
- 実行スクリプト (readActor を含む)
です。 Read-Write Lock パターンのキモは Data クラスです。 Data オブジェクトからの読み書きに Read-Write Lock パターンを使っています。 Data 以外のクラス、スクリプトは Read-Write Lock パターンとあまり関係ありません。
Data クラス
Read-Write Lock パターンを実装したクラス。 ReadWriteLock オブジェクトや Lock オブジェクトを保持し、read(), write() メソッド内で対応するロックの取得・解放を行っています:
import java.util.concurrent.locks.ReadWriteLock import java.util.concurrent.locks.ReentrantReadWriteLock import java.util.concurrent.locks.Lock class Data { private final char[] buffer private final int n private final ReadWriteLock lock = new ReentrantReadWriteLock(true) private final Lock readLock = lock.readLock() private final Lock writeLock = lock.writeLock() Data(int size){ this.n = size this.buffer = ['*']*n as char[] } char[] read(){ this.readLock.lock() // 読み込みのロック取得 try{ char[] newBuffer = Arrays.copyOf(this.buffer, n) slowly() return newBuffer }finally{ this.readLock.unlock() // 読み込みのロック解放 } } void write(char c){ this.writeLock.lock() // 書き込みのロック取得 try{ (0..<n).each{ this.buffer[it] = c } slowly() }finally{ this.writeLock.unlock() // 書き込みのロック解放 } } private void slowly(){ Thread.sleep 50 } }
- ReentrantReadWriteLock クラスのコンストラクタに渡している boolean 値はフェアネスを設定しています。 これが true だと、長く待たされているスレッドに優先的にロックが渡されるようになります。
WriterActor クラス
データ書き込みを行うスレッドを Actor で実装:
import groovyx.gpars.actor.DefaultActor class WriterActor extends DefaultActor{ private static final Random RANDOM = new Random() private final Data data private final String filler private int index = 0 WriterActor(Data data, String filler){ this.data = data this.filler = filler } @Override void act(){ loop{ char c = nextChar() this.data.write(c) Thread.sleep RANDOM.nextInt(3000) } } private char nextChar(){ char c = this.filler.charAt(this.index) this.index = (this.index + 1) % this.filler.size() return c } }
実行スクリプト
実行スクリプト。 データを読み込むスレッドを Actor として作成もしています:
import groovyx.gpars.actor.Actors final Data data = new Data(10) def actors = [] 6.times{ actors << Actors.actor{ loop{ char[] readBuffer = data.read() println "${Thread.currentThread().name} reads ${String.valueOf(readBuffer)}" } } } actors << new WriterActor(data, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ').start() actors << new WriterActor(data, 'abcdefghijklmnopqrstuvwxyz').start() actors*.join()
java.util.concurrent パッケージのクラス群のお陰で、Read-Write Lock パターン関連の実装はかなり簡単にできますね。 なんか、チャチな Actor 実装を書く練習と化しております。
追記
id:uehaj 氏にご指摘いただいた Groovy の @WithReadLock, @WithWriteLock アノテーションを使ったサンプル。 上記のコードをもっと手軽に書けます:
import groovy.transform.WithReadLock import groovy.transform.WithWriteLock class GroovyData { private final char[] buffer private final int n GroovyData(int size){ this.n = size this.buffer = ['*']*n as char[] } @WithReadLock char[] read(){ char[] newBuffer = Arrays.copyOf(this.buffer, n) slowly() return newBuffer } @WithWriteLock void write(char c){ (0..<n).each{ this.buffer[it] = c } slowly() } private void slowly(){ Thread.sleep 50 } }
これらのアノテーションが付与されたメソッドは、上記のサンプルの対応するメソッドとほぼ同じコードに AST 変換されるようです。
参考 URL

増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編
- 作者: 結城浩
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2006/03/21
- メディア: 大型本
- 購入: 15人 クリック: 287回
- この商品を含むブログ (199件) を見る

- 作者: 関谷和愛,上原潤二,須江信洋,中野靖治
- 出版社/メーカー: 技術評論社
- 発売日: 2011/07/06
- メディア: 単行本(ソフトカバー)
- 購入: 6人 クリック: 392回
- この商品を含むブログ (152件) を見る