【Kotlin/Android】ReentrantLock/ReentrantReadWriteLockでスレッドセーフ(排他制御)の実装方法

この記事からわかること
- Android Studio/Kotlinでスレッドセーフ(排他制御)を実装するには?
- ReentrantLock/ReentrantReadWriteLockの使い方
index
[open]
\ アプリをリリースしました /
環境
- Android Studio:Meerkat
- Kotlin:2.0.21
- Mac M1:Sequoia 15.4
スレッドセーフ(排他制御)な実装
Kotlinで複数のスレッドからの同時アクセスを防ぐための手段はいくつか存在します。今回はその中でもReentrantLock
/ReentrantReadWriteLock
の使い方をまとめていきます。
ReentrantLock
ReentrantLock
を使用して排他制御を行うにはReentrantLock
をインスタンス化して、スレッドセーフにしたい処理の前にlock
/unlock
メソッドを使用してロック/アンロックします。unlock
を呼び忘れるとデッドロック状態になってしまうのでtry ~ finally
構文を使用して処理の最後に必ず呼ばれるような仕組みにしておいた方が安全です。
val lock = ReentrantLock()
var count = 0
fun safeIncrement() {
// ロック
lock.lock()
try {
count++
} finally {
// ロックを解除
lock.unlock()
}
}
他のスレッドで処理を実行中でReentrantLock
がロックされている状態の場合に別のスレッドからlock
を呼び出すとロック中のスレッドからの処理がアンロックされるまで待機します。
fun safeIncrementDelay(delay: Long = 3000) {
lock.lock()
try {
Thread.sleep(delay)
count++
println(count)
} finally {
lock.unlock()
}
}
fun main() {
println("START $count")
// スレッドAで遅延インクリメント
thread(name = "Thread-A") {
println("Thread-A START")
safeIncrementDelay(3000)
println("Thread-A END $count")
}
// スレッドBでインクリメント
thread(name = "Thread-B") {
println("Thread-B START")
safeIncrement()
println("Thread-B END $count")
}
Thread.sleep(5000)
}
例えば上記のように実行した場合でも出力は期待通りにインクリメントされて動作します。ただこれをスレッドセーフ機構を使用しないとスレッドBのsafeIncrement
が先に実行されスレッドBの出力はThread-B END 1
になってしまいます。
START 0
Thread-A START
Thread-B START
1
Thread-A END 1
Thread-B END 2
アンロックを待機しない
別スレッドでの処理中の場合にアンロックを待機したくない場合はtryLock
メソッドを使用します。Boolean
でロック中かどうかを取得することができるのでハンドリングしてあげればOKです。
fun safeIncrementNotWait() {
if (lock.tryLock()) {
try {
count++
} finally {
lock.unlock()
}
} else {
println("ロック失敗:すでに誰かがロック中")
}
}
tryLock
の引数には待機秒数を指定することが可能です。
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
count++
} finally {
lock.unlock()
}
} else {
println("ロック失敗:1秒待っても取れなかった")
}
複数回のロックとロックなしでのアンロック
lock
メソッドは複数回実行することが可能になっています。呼び出した分だけロックされてしまうので解除する際もロックした回数分unlock
を呼び出す必要があります。
fun safeIncrement() {
// ロック
lock.lock()
// 2重でロック
lock.lock()
try {
count++
} finally {
// ロックを解除
lock.unlock()
// 2重で解除
lock.unlock()
}
}
またlock
を呼んでいないのにunlock
を呼び出すとjava.lang.IllegalMonitorStateException
をスローします。
ReentrantReadWriteLock
ReentrantReadWriteLock
もスレッドアクセスをブロックできる機構も持っていますが「複数のスレッドから同時の取得は許可し、書込は排他制御」を行うことが特徴です。
使用方法はReentrantReadWriteLock
をインスタンス化し、readLock
で読込用のロック機構をwriteLock
で書込用のロック機構を取得します。
val rwLock = ReentrantReadWriteLock()
val readLock = rwLock.readLock()
val writeLock = rwLock.writeLock()
読み込みを行いたい場合はreadLock.lock()
でロックします。読み込みは複数スレッドからでもアクセスは可能ですが、読み込み中に書き込まれないようにロックします。
fun readData() {
readLock.lock()
try {
println("読み取り: $count")
} finally {
readLock.unlock()
}
}
書き込みを行いたい場合はwriteLock.lock()
でロックします。
fun writeData(value: Int) {
writeLock.lock()
try {
println("書き込み中: $value")
count = value
} finally {
writeLock.unlock()
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。