Skip to content

Part V: 同期と排他制御

概要

本章では、銀行口座を例に同期とデッドロック回避を学びます。


銀行口座クラス

class BankAccount(initialBalance: Int):
  private val lock = new Object
  private var balance: Int = initialBalance

  def getBalance: Int = lock.synchronized {
    balance
  }

  def deposit(amount: Int): Unit = lock.synchronized {
    balance += amount
  }

  def withdraw(amount: Int): Boolean = lock.synchronized {
    if balance >= amount then
      balance -= amount
      true
    else
      false
  }

デッドロック回避

object BankAccount:
  /** Transfer money between accounts atomically, avoiding deadlock */
  def transfer(from: BankAccount, to: BankAccount, amount: Int): Boolean =
    // Always lock in consistent order to avoid deadlock
    val (first, second) = if System.identityHashCode(from) < System.identityHashCode(to)
      then (from, to)
      else (to, from)

    first.lock.synchronized {
      second.lock.synchronized {
        if from.balance >= amount then
          from.balance -= amount
          to.balance += amount
          true
        else
          false
      }
    }

デッドロックとは

2 つのスレッドが互いのロックを待つ状態:

Thread A: lock(account1) -> waiting for lock(account2)
Thread B: lock(account2) -> waiting for lock(account1)

解決策: ロック順序の統一

System.identityHashCode を使用してロック順序を決定することで、すべてのスレッドが同じ順序でロックを取得します。


並行テスト

test("BankAccount should handle concurrent transfers safely"):
  val account1 = new BankAccount(1000)
  val account2 = new BankAccount(1000)

  val executor = Executors.newFixedThreadPool(10)
  val latch = new CountDownLatch(100)

  (1 to 50).foreach { _ =>
    executor.submit(new Runnable:
      def run(): Unit =
        BankAccount.transfer(account1, account2, 10)
        latch.countDown()
    )
    executor.submit(new Runnable:
      def run(): Unit =
        BankAccount.transfer(account2, account1, 10)
        latch.countDown()
    )
  }

  latch.await()
  executor.shutdown()

  // Total balance should be preserved
  (account1.getBalance + account2.getBalance) shouldBe 2000

次のステップ

Part VI では、ノンブロッキング I/O を学びます。