ReentrantLock.unlock(): примеры (JAVA)

Метод unlock() в ReentrantLock - разбор поведения
Раздел: Многопоточность, Блокировки
ReentrantLock.unlock(): void

Описание метода

Метод ReentrantLock.unlock() относится к классу java.util.concurrent.locks.ReentrantLock. Он используется для освобождения захваченного блокировкой потока. В отличие от синхронизированного блока (intrinsic lock), ReentrantLock предоставляет дополнительные возможности: справедливость, возможность прерывания при ожидании, попытки захвата с таймаутом и получение состояния захвата.

Аргументы: отсутствуют. Сигнатура: public void unlock().

Возвращаемое значение: нет (тип void). Поведение: при вызове уменьшает счетчик удержания блокировки (hold count) для вызывающего потока. Если счетчик становится равен нулю, блокировка полностью освобождается и может быть захвачена другим потоком.

Исключения и особые случаи:

  • Если текущий поток не владеет блокировкой, бросается IllegalMonitorStateException. Это самая частая ошибка при некорректном использовании unlock().
  • Метод не блокирует; он немедленно возвращает управление либо бросает исключение.
  • ReentrantLock поддерживает повторный захват: один и тот же поток может вызвать lock() несколько раз, тогда требуется соответствующее число вызовов unlock() для полного освобождения.

Сопутствующие методы класса полезны при отладке и управлении: isHeldByCurrentThread(), getHoldCount(), isLocked(), tryLock(), lockInterruptibly() и newCondition(). Они не изменяют поведение unlock(), но помогают избежать ошибок.

Короткие примеры использования

1) Базовый паттерн с try/finally

import java.util.concurrent.locks.ReentrantLock;

public class Example1 {
    private static final ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        lock.lock();
        try {
            System.out.println("Внутри критической секции");
        } finally {
            lock.unlock();
        }
    }
}
Внутри критической секции

2) Неправильный вызов unlock() из потока, не владеющего блокировкой

import java.util.concurrent.locks.ReentrantLock;

public class Example2 {
    private static final ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
            lock.unlock();
        });
        t1.start();
        Thread.sleep(100); // пусть t1 захватит
        // второй поток пытается отпустить без захвата
        Thread t2 = new Thread(() -> {
            try {
                lock.unlock();
            } catch (Exception e) {
                System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
            }
        });
        t2.start();
        t1.join(); t2.join();
    }
}
IllegalMonitorStateException: null

3) Повторный захват и соответствующее количество освобождений

import java.util.concurrent.locks.ReentrantLock;

public class Example3 {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        lock.lock(); // повторный захват
        System.out.println("Hold count: " + lock.getHoldCount());
        lock.unlock();
        System.out.println("Hold count after one unlock: " + lock.getHoldCount());
        lock.unlock();
        System.out.println("Hold count after second unlock: " + lock.getHoldCount());
    }
}
Hold count: 2
Hold count after one unlock: 1
Hold count after second unlock: 0

4) tryLock и возможный отказ в захвате

import java.util.concurrent.locks.ReentrantLock;

public class Example4 {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        boolean got = lock.tryLock();
        System.out.println("Попытка захвата без ожидания: " + got);
        lock.unlock();
    }
}
Попытка захвата без ожидания: false

Аналоги и варианты в Java

  • synchronized (встроенный монитор): проще в использовании, автоматически освобождается при выходе из блока; не предоставляет гибких опций, таких как tryLock с таймаутом или справедливость.
  • ReentrantReadWriteLock: полезен, когда требуется разделять чтение и запись; чтение допускает конкурентный доступ, запись - эксклюзивный.
  • StampedLock: предлагает оптимистичные чтения и более тонкое управление блокировками, но не является полностью reentrant и требует осторожности.
  • Semaphore и CountDownLatch: используются для управления количеством одновременно выполняемых операций или ожидания событий, но не для взаимного исключения в классическом виде reentrant-lock.

Выбор между ReentrantLock и встроенным монитором зависит от требований: если нужна гибкость (tryLock, таймаут, interruptible lock, справедливость), ReentrantLock предпочтительнее; если нужно простое блокирование, synchronized обычно проще и безопаснее.

Альтернативы в других языках

  • Python: threading.RLock() имеет методы acquire() и release(), поведение аналогично ReentrantLock. Пример:
import threading
r = threading.RLock()

r.acquire()
try:
    print('Внутри RLock')
finally:
    r.release()
Внутри RLock
  • C#: встроенная конструкция lock(obj) использует Monitor.Enter/Exit; явный Monitor.Exit(obj) эквивалентен unlock, но требует корректного владения. Пример:
using System;
using System.Threading;

class Program {
    static object obj = new object();
    static void Main() {
        Monitor.Enter(obj);
        try {
            Console.WriteLine("В секции");
        } finally {
            Monitor.Exit(obj);
        }
    }
}
В секции
  • Go: sync.Mutex с методами Lock() и Unlock(). Unlock() вызывает панику при некорректном вызове со стороны не владеющего горутины. Пример:
package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex
    mu.Lock()
    fmt.Println("Locked")
    mu.Unlock()
    fmt.Println("Unlocked")
}
Locked
Unlocked
  • JavaScript: в однопоточном окружении Node.js локи обычно не требуются; при работе с worker threads используются сторонние библиотеки (например async-mutex) с API acquire()/release() или Promise-обертками. Пример концептуальный:
// npm i async-mutex
const { Mutex } = require('async-mutex');
const mutex = new Mutex();

mutex.acquire().then(release => {
  console.log('В секции');
  release();
});
В секции
  • PHP: в стандартном исполнении потоков нет; при использовании расширения pthreads или системных семафоров используются соответствующие API (mutex/sem) с похожими концепциями but синтаксис и поведение отличаются.
  • Kotlin: использует те же классы Java, например java.util.concurrent.locks.ReentrantLock, поведение совпадает.
  • SQL: в СУБД используются advisory locks (например, pg_advisory_lock в PostgreSQL). Они работают на уровне БД и отличаются от блокировок в памяти приложения.
  • Lua: нативных потоков нет, используются сторонние библиотеки (Lua Lanes и др.), где предоставляются свои механизмы синхронизации.

Типичные ошибки при использовании unlock()

  • Отсутствие finally при освобождении блокировки. Результат: блокировка может не освободиться при исключении, что ведет к дедлоку. Пример:
ReentrantLock lock = new ReentrantLock();
lock.lock();
// Исключение до unlock
throw new RuntimeException("Ошибка");
// unlock() никогда не вызовется
(в результате возникнет зависший ресурс, методы ожидающие lock будут блокироваться)
  • Вызов unlock() из потока, который не удерживает блокировку. Результат: IllegalMonitorStateException. Пример выше (см. Examples 2).
  • Недостаточное количество вызовов unlock() после повторных lock(). Результат: блокировка остается удержанной, другие потоки блокируются. Пример: lock.lock(); lock.lock(); lock.unlock(); - требуется еще один unlock().
  • Ожидание внутри критической секции без освобождения блокировки (например, долгие блокирующие операции) приводит к снижению параллелизма и возможным дедлокам.
  • Попытка использовать Condition.signal()/await() без соблюдения правил владения lock: await освобождает lock и затем заново захватывает; signal требует владения lock при вызове signal(). Неправильный порядок вызывает IllegalMonitorStateException или логические ошибки.

Изменения и история

Класс ReentrantLock и метод unlock() введены в Java 5 (JDK 1.5) в рамках пакета java.util.concurrent. С тех пор сигнатура public void unlock() и базовое поведение не менялись. Добавлялись вспомогательные методы в разные релизы (например, getHoldCount(), isHeldByCurrentThread(), isLocked()), но сам алгоритм освобождения и возможные исключения остались прежними. Новых изменений в семантике unlock() в последних версиях не отмечено.

Расширенные и нестандартные примеры

1) Использование Condition: await() освобождает блокировку и затем восстанавливает владение при пробуждении. Важно вызывать unlock() в finally.

Пример java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition cond = lock.newCondition();
    private boolean ready = false;

    public void awaiter() throws InterruptedException {
        lock.lock();
        try {
            while (!ready) {
                cond.await(); // временно освобождает lock
            }
            System.out.println("Условие выполнено");
        } finally {
            lock.unlock();
        }
    }

    public void signaler() {
        lock.lock();
        try {
            ready = true;
            cond.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ConditionExample ex = new ConditionExample();
        Thread t = new Thread(() -> {
            try { ex.awaiter(); } catch (InterruptedException ignored) {}
        });
        t.start();
        Thread.sleep(100);
        ex.signaler();
    }
}
Условие выполнено

2) Обёртка для try-with-resources: создание AutoCloseable-адаптера для автоматического вызова unlock()

Пример java
import java.util.concurrent.locks.ReentrantLock;

public class LockAutoCloseable implements AutoCloseable {
    private final ReentrantLock lock;
    public LockAutoCloseable(ReentrantLock lock) {
        this.lock = lock;
        this.lock.lock();
    }
    @Override
    public void close() {
        lock.unlock();
    }

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        try (LockAutoCloseable lac = new LockAutoCloseable(lock)) {
            System.out.println("Внутри try-with-resources");
        }
    }
}
Внутри try-with-resources

3) tryLock с таймаутом и реакция на отказ в захвате

Пример java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TryLockTimeout {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        Thread t = new Thread(() -> {
            try {
                if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
                    try { System.out.println("Захват выполнен"); } finally { lock.unlock(); }
                } else {
                    System.out.println("Не удалось захватить за отведенное время");
                }
            } catch (InterruptedException e) {
                System.out.println("Поток прерван");
            }
        });
        t.start();
        Thread.sleep(1000);
        lock.unlock();
        t.join();
    }
}
Не удалось захватить за отведенное время

4) Справедливость (fair) против несправедливого режима: в справедливом режиме потоки получают блокировку в порядке очереди ожидания. Демонстрация может показать изменение порядка обслуживания, однако в небольших примерах результат может варьироваться из-за планировщика.

5) Рекурсивные вызовы: демонстрация использования lock/unlock через несколько уровней вызовов функции. Важно, чтобы количество unlock соответствовало количеству lock.

Пример java
import java.util.concurrent.locks.ReentrantLock;

public class RecursiveExample {
    private static final ReentrantLock lock = new ReentrantLock();
    public static void recurse(int depth) {
        lock.lock();
        try {
            System.out.println("Depth: " + depth);
            if (depth > 0) recurse(depth - 1);
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        recurse(3);
    }
}
Depth: 3
Depth: 2
Depth: 1
Depth: 0

6) Нестандартное: контроль состояния блокировки в отладочных логах без вмешательства в логику синхронизации (использовать isHeldByCurrentThread() и getHoldCount() только для диагностики). Пример пропускается по компактности, но рекомендуется вызывать в безопасном месте, чтобы избежать TOCTOU-ошибок.

джава ReentrantLock.unlock() function comments

En
ReentrantLock.unlock() Освобождает блокировку