WeCodeAcademy

Step 5: Many threads at once

A real bank handles many requests at the same moment. Two threads each doing cents += 1 can both read the same old value and both write the same new one, so one deposit is lost (a race condition). It shows up randomly, which makes it one of the hardest bugs to find. The cure is to make the read-modify-write atomic: synchronized lets only one thread in at a time, and AtomicLong does it with a single hardware instruction. Eight threads each make 10,000 deposits of one cent:

ExampleJava
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class Main {
  static class SafeAccount {
    private long cents = 0;
    // synchronized: only one thread at a time may run this method on this object
    synchronized void deposit(long amount) { cents += amount; }
    synchronized long balance() { return cents; }
  }

  public static void main(String[] args) throws Exception {
    SafeAccount account = new SafeAccount();
    AtomicLong atomic = new AtomicLong();
    ExecutorService pool = Executors.newFixedThreadPool(8);

    for (int t = 0; t < 8; t++) {
      pool.submit(() -> {
        for (int i = 0; i < 10_000; i++) {
          account.deposit(1);
          atomic.incrementAndGet();
        }
      });
    }
    pool.shutdown();
    pool.awaitTermination(10, TimeUnit.SECONDS);

    System.out.println("synchronized: " + account.balance());
    System.out.println("atomic:       " + atomic.get());
    System.out.println("expected:     " + 8 * 10_000);
  }
}
Output
synchronized: 80000
atomic:       80000
expected:     80000

Both counters show exactly 80000. Without the protection the answer would vary from run to run and usually fall short. Deadlock is the next danger: if transfer A→B locks A then B while transfer B→A locks B then A, each can wait for the other forever. The standard fix is to always lock in a fixed order (lower id first):

ExampleJava
import java.util.concurrent.locks.*;

public class Main {
  static class Account {
    final int id;
    long cents;
    final Lock lock = new ReentrantLock();
    Account(int id, long cents) { this.id = id; this.cents = cents; }
  }

  // Always lock the account with the lower id first. Two transfers in opposite directions
  // then can never wait for each other forever (a "deadlock").
  static void transfer(Account a, Account b, long amount) {
    Account first = a.id < b.id ? a : b;
    Account second = a.id < b.id ? b : a;
    first.lock.lock();
    try {
      second.lock.lock();
      try {
        if (a.cents >= amount) { a.cents -= amount; b.cents += amount; }
      } finally {
        second.lock.unlock();
      }
    } finally {
      first.lock.unlock();
    }
  }

  public static void main(String[] args) throws Exception {
    Account x = new Account(1, 100_000);
    Account y = new Account(2, 100_000);

    Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) transfer(x, y, 1); });
    Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) transfer(y, x, 1); });
    t1.start();
    t2.start();
    t1.join();
    t2.join();

    System.out.println("x=" + x.cents + " y=" + y.cents + " total=" + (x.cents + y.cents));
  }
}
Output
x=100000 y=100000 total=200000

Ten thousand opposite transfers ran without freezing, and the total is unchanged: 200000.

Quick check

What is a race condition?

Finish the quick check (and any code exercise) to complete this lesson, or mark it done yourself.

Tip: use the left and right arrow keys to move between lessons.