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

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

この記事からわかること

  • Android Studio/Kotlinスレッドセーフ(排他制御)を実装するには?
  • ReentrantLock/ReentrantReadWriteLock使い方

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

スレッドセーフ(排他制御)な実装

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()
    }
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

Search Box

Sponsor

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article