Понимание тупика на простом примере

Я работаю над пониманием основ взаимоблокировки, поэтому я придумал код ниже. У меня есть два потока, получающие блокировки в противоположном порядке, но они не блокируются. Когда я запускаю его, я вижу все распечатки. Что я делаю неправильно?

public class DeadlockBasics {
  private Lock lockA = new ReentrantLock();
  private Lock lockB = new ReentrantLock();

  public static void main(String[] args) {
    DeadlockBasics dk = new DeadlockBasics();
    dk.execute();
  }

  private void execute() {
    new Thread(this::processThis).start();
    new Thread(this::processThat).start();
  }

  // called by thread 1
  public void processThis() {
    lockA.lock();
    // process resource A
    System.out.println("resource A -Thread1");

    lockB.lock();
    // process resource B
    System.out.println("resource B -Thread1");

    lockA.unlock();
    lockB.unlock();
  }

  // called by thread 2
  public void processThat() {
    lockB.lock();
    // process resource B
    System.out.println("resource B -Thread2");

    lockA.lock();
    // process resource A
    System.out.println("resource A -Thread2");

    lockA.unlock();
    lockB.unlock();
  }
}

4 ответа

Решение

Прежде всего, нет гарантии, какие потоки запускаются первыми. Чтобы получить тупик, один из потоков должен взять блокировку на lockA а затем второй поток должен взять блокировку на lockB или наоборот.

public void processThis() {
    lockA.lock();
    // here the control should be switched to another thread
    System.out.println("resource A -Thread1");

    lockB.lock();
    ...

Но может не хватить времени для переключения между потоками, потому что у вас всего несколько строк кода. Это слишком быстро. Чтобы эмулировать долгую работу, добавьте задержку перед второй блокировкой для обоих методов.

lockA.lock();
Thread.sleep(200);  // 200 milis

Тогда второй поток сможет заблокировать lockB до первого релиза оба

Это действительно может привести к взаимоблокировке, но не всегда, например, если processThis () полностью выполняется, а затем processThat () или наоборот, тупиковая ситуация отсутствует. Вы можете попробовать добавить Thread.delay(100) или Thread.yield (), чтобы направить выполнение потоков в тупик или даже снять разблокировку с определенного тупика.

Два момента:

  1. Отпустите замки в порядке, обратном их приобретению. То есть, processThis следует поменять порядок снятия замков. Для вашего примера порядок не имеет значения. Но если processThis попытался установить новую блокировку на A, прежде чем снять блокировку на B, снова может произойти тупик. В более общем плане вам будет проще думать о блокировках, учитывая их объем и избегая перекрывающихся, но не ограничивающих область.
  2. Чтобы лучше выделить проблему, я бы позвонил wait после получения первой блокировки в каждом из потоков, и иметь execute запустить оба потока, затем вызвать notify на обе темы.

Ваш код является хорошим примером мертвой блокировки, поскольку ReenttrantLock - это блокировка взаимного исключения с тем же поведением, что и неявная блокировка доступа к монитору с использованием синхронизированного. Однако вы не видите тупик из-за этой части:

private void execute() {
      new Thread(this::processThis).start();
      new Thread(this::processThat).start();
}

После создания и запуска первого потока создание второго потока занимает некоторое время. Для создания новой нити JVM требуется около 50, а может и меньше, звучит очень коротко, но этого достаточно, чтобы завершить первую нить, и, следовательно, не будет мертвой блокировки.

Я добавил Thread.sleep(); в ваш код, чтобы оба потока могли выполняться как-то параллельно.

package com.company;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class DeadlockBasics {
    private Lock lockA = new ReentrantLock();
    private Lock lockB = new ReentrantLock();

    public static void main(String[] args) {
        DeadlockBasics dk = new DeadlockBasics();
        dk.execute();
    }

    private void execute() {
        new Thread(this::processThis).start();
        new Thread(this::processThat).start();
    }

    // called by thread 1
    private void processThis() {
        lockA.lock();
        // process resource A
        try {
            Thread.sleep(1000); //Wait for thread 2 to be executed
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread 1 will own lock a");

        lockB.lock();
        // process resource B
        System.out.println("Thread 1 will own lock b");

        lockA.unlock();
        lockB.unlock();

        // Both locks will now released from thread 1
    }

    // called by thread 2
    private void processThat() {
        lockB.lock();
        // process resource B
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread 2 will own lock b");

        lockA.lock();
        // process resource A
        System.out.println("Thread 2 will own lock a");

        lockA.unlock();
        lockB.unlock();

        // Both locks are released by thread 2
    }
}
Другие вопросы по тегам