synchronizedとは
そもそもJavaでは特定のオブジェクトに関する操作をスレッドセーフにするためにsynchronized
という修飾子が用意されています。
例えば以下のように付与します。
public class Data {
int x = 0;
int y = 0;
public synchronized void inc() { // synchronized method
x += 1;
y += 1;
}
public void dec() {
synchronized (this) { // synchronized statement
x -= 1;
y -= 1;
}
}
}
synchronized
が付与されたメソッドを定義すると、メソッドの呼び出し元のスレッドがこのインスタンス(この場合Dataインスタンス)に対してロックを取ります。一度ロックが取得されると、ロックが開放されるまで他のスレッドがこのインスタンスに対するロックを必要とする処理をブロックされるようになります。使い方にはsynchronized methodとsynchronized statementの2種類があります。
上記のinc()メソッドとdec()メソッドはそれぞれx,yに1を加える/引くメソッドなのですが、synchronizedによって同期を取らないと複数のスレッドからの呼び出しによってxとyの値が異なる状態が発生し得ます。
これはJavaプログラマーにおいてはかなり重要な要素なのですが、Kotlinではsynchronized修飾子は用意されていません。その代わり、@Synchronizedアノテーションとsynchronizedインライン関数が用意されています。
[https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-synchronized/:title]
class Data(var x: Int, var y: Int) {
@Synchronized
fun inc() {
x += 1
y += 1
}
fun dec() {
synchronized(this) {
x -= 1
y -= 1
}
}
}
これでほぼ等価なJavaとKotlinで互換したコードが書けましたね。
ReentrantLockについて
ReentrantLockとは、java.util.concurrentに収められている並列処理のためのロック機構です。
synchronized
と似たような同期を実現することができます。
利用する時はReentrantLockのインスタンスを生成し、lock()とunlock()によって取得と開放を行います。
他のスレッドがロックを取得していた場合はlock()でブロックされます。
unlock()がされないことがないように、JavaDocにはtry-finallyを使用することが勧められています。
public class Data {
int x = 0;
int y = 0;
Lock lock = ReentrantLock(true);
public void inc() {
lock.lock();
try {
x += 1;
y += 1;
} finally {
lock.unlock()
}
}
}
Kotlinの場合はもっとスマートに、withLockインライン関数を用いて以下のように書くことができます。
withLockではブロック内に入る前にロックが取得され、出る際にロックが開放されます。
class Data(var x: Int, var y: Int) {
val lock = ReentrantLock(true)
fun inc() {
lock.withLock {
x += 1
y += 1
}
}
}
メリット・デメリット
結局どちらの実装で同期を取るのがベターなのでしょうか。
それを知るには以下に目を通すとよいでしょう。
https://www.ibm.com/developerworks/java/library/j-jtp10264/
https://stackoverflow.com/questions/11821801/why-use-a-reentrantlock-if-one-can-use-synchronizedthis
ReentrantLockを使用するべきなのはどのような場合か?という問に対して、以下のように答えています。
The answer is pretty simple -- use it when you actually need something it provides that synchronized doesn't, like timed lock waits, interruptible lock waits, non-block-structured locks, multiple condition variables, or lock polling.
勝手に日本語訳をつけると以下のようになります。
答えは極めて単純、
synchronized
ができないことが必要になったときだ。例えばタイムアウト付きwait、割り込み可能なwait、ブロック構造でないロックの書き方、複数の条件変数、ロックの状態確認など。
結局どちらを選ぶべきかという議論に対して、synchronizedはスレッド間の同期に最低限の機能を提供しているから十分だと言うこともできるし、より柔軟な同期機能を利用したいがためにReentrantLockを利用するのもありということですね。
あとがき
かなり古い本で勉強していたらjava.util.concurrentのAPIについてほとんど触れられていなくて、今風の実装を調べ直す羽目になりました。とはいえJ2SE5.0が出たのが2004年9月30日だそうで、Java界隈にはjava.util.concurrentに関するノウハウがかなり蓄積されていることだと思います。