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

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

Описание ReentrantLock.lock()

Метод ReentrantLock.lock() класса java.util.concurrent.locks.ReentrantLock устанавливает взаимоисключающую блокировку для текущего потока. Если блокировка свободна, поток получает её и продолжает выполнение. Если блокировка удерживается другим потоком, текущий поток блокируется до освобождения. Класс является реентрантным: поток, уже владеющий блокировкой, может выполнить lock() повторно без блокировки самого себя.

Основные варианты работы блокировок в API Lock/ReentrantLock:

  • void lock() - блокирующий вызов; не принимает аргументов и не возвращает значения. Не реагирует на прерывания во время ожидания.
  • void lockInterruptibly() throws InterruptedException - блокирующий вызов, но реагирует на прерывание: при прерывании бросает InterruptedException.
  • boolean tryLock() - неблокирующий: пытается получить блокировку и сразу возвращает true, если удалось, или false, если нет.
  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException - пытается получить блокировку в течение указанного времени; возвращает true, если удалось, иначе false; реагирует на прерывания.
  • void unlock() - освобождает блокировку; при вызове потоком, который не владеет блокировкой, бросает IllegalMonitorStateException. Возвращаемого значения нет.

Конструкторы ReentrantLock() и ReentrantLock(boolean fair) позволяют задать справедливость распределения доступа. По умолчанию (fair = false) используется неблагоприятный для порядка «быстрый» режим, который может давать большую пропускную способность. При fair = true ожидающие потоки получают доступ в порядке очереди.

Возвращаемые значения и исключения кратко:

  • lock(), unlock() - возвращают void; unlock() может привести к IllegalMonitorStateException.
  • lockInterruptibly(), tryLock(long, TimeUnit) - могут бросить InterruptedException.
  • tryLock(), tryLock(long, TimeUnit) - возвращают boolean.

Когда применяется

Применение целесообразно при необходимости явного контроля порядка захвата и освобождения блокировки, при работе с Condition (await/signal), при необходимости поддерживать справедливость или при сложной логике синхронизации, которую нельзя выразить с помощью ключевого слова synchronized.

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

Пример 1 - базовая блокировка и разблокировка

import java.util.concurrent.locks.ReentrantLock;

public class BasicLock {
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        lock.lock();
        try {
            System.out.println("Критическая секция");
        } finally {
            lock.unlock();
        }
    }
}
Критическая секция

Пример 2 - tryLock() без блокировки

import java.util.concurrent.locks.ReentrantLock;

public class TryLockExample {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        if (lock.tryLock()) {
            try {
                System.out.println("Получил блокировку");
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("Не удалось получить блокировку");
        }
    }
}
Получил блокировку

Пример 3 - tryLock с таймаутом

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

public class TryLockTimeout {
    public static void main(String[] args) throws Exception {
        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 - lockInterruptibly()

import java.util.concurrent.locks.ReentrantLock;

public class InterruptibleExample {
    public static void main(String[] args) throws Exception {
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        Thread t = new Thread(() -> {
            try {
                System.out.println("Ожидание блокировки");
                lock.lockInterruptibly();
                try { System.out.println("Получено"); } finally { lock.unlock(); }
            } catch (InterruptedException e) {
                System.out.println("Ожидание прервано");
            }
        });
        t.start();
        Thread.sleep(200);
        t.interrupt();
        lock.unlock();
        t.join();
    }
}
Ожидание блокировки
Ожидание прервано

Аналоги в Java и их особенности

  • synchronized - встроенный механизм языка. Проще в использовании и безопасен для большинства задач, но не предоставляет Condition, справедливость и гибкие tryLock/timeout возможности.
  • ReentrantReadWriteLock - разделяет чтение и запись: несколько читателей или один писатель. Предпочтительно при высоком соотношении чтений к записам.
  • StampedLock - более продвинутый механизм для случаев с оптимистичным чтением и возможностью апгрейда блокировки; эффективен при специфичных шаблонах доступа.
  • Semaphore - подсчетная семафорная блокировка; позволяет ограничивать количество параллельных исполнений, не привязана к владельцу.

Выбор зависит от требований: для простоты и автоматического освобождения при выходе из блокировки предпочтительнее synchronized; для сложной координации потоков и использования Conditions - ReentrantLock или ReentrantReadWriteLock.

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

Python

import threading
lock = threading.RLock()

lock.acquire()
try:
    print('Критическая секция')
finally:
    lock.release()
Критическая секция

Разница: RLock похож на ReentrantLock; acquire может принимать таймаут и реагирует на прерывания потоков по-иному, поскольку в Python нет прерывания по InterruptedException.

Go

package main

import (
    "fmt"
    "sync"
)

var mu sync.Mutex

func main() {
    mu.Lock()
    fmt.Println("Критическая секция")
    mu.Unlock()
}
Критическая секция

Go предлагает sync.Mutex, не реентрантный по умолчанию; для реентрантности требуется иная логика.

C#

using System;
using System.Threading;

class Program {
  static object sync = new object();
  static void Main() {
    lock(sync) {
      Console.WriteLine("Критическая секция");
    }
  }
}
Критическая секция

В C# ключевое слово lock использует Monitor. Есть Mutex, SemaphoreSlim и другие механизмы. Reentrancy контролируется Monitor.

JavaScript

JS в браузере однопоточный; в Node.js возможны worker threads и shared memory с Atomics. Часто используют асинхронные mutex-реализации на промисах:

class Mutex {
  constructor(){ this._q = Promise.resolve(); }
  lock(){
    let p = this._q;
    let release;
    this._q = new Promise(res => release = res);
    return p.then(() => release);
  }
}

(async ()=>{
  const m = new Mutex();
  const release = await m.lock();
  console.log('Критическая секция');
  release();
})();
Критическая секция

PHP

В чистом PHP нет встроенных потоков в стандартной установке. При использовании расширения pthreads доступен Mutex. В веб-контексте часто применяются внешние механизмы: блокировки в БД или файловые лocks.

SQL

В реляционных БД есть свои механизмы блокировок и advisory locks (например, pg_try_advisory_lock), которые обеспечивают координацию между процессами, а не между потоками JVM.

Kotlin

Kotlin на JVM использует те же примитивы, что Java. Kotlin coroutines используют другие стратегии синхронизации (Mutex из kotlinx.coroutines) для неблокирующего ожидания.

Типичные ошибки и последствия

1. Пропуск unlock в finally

ReentrantLock lock = new ReentrantLock();
lock.lock();
// если забыть finally, при исключении блокировка останется
// и другие потоки заблокируются навсегда
// неправильный код
if (true) throw new RuntimeException("ошибка");
lock.unlock();
Программа может зависнуть; другие потоки не получат блокировку

2. Вызов unlock без владения

ReentrantLock lock = new ReentrantLock();
try {
    lock.unlock();
} catch (Exception e) {
    System.out.println(e.getClass().getSimpleName());
}
IllegalMonitorStateException

3. Возможная потеря производительности при справедливом режиме

Установка new ReentrantLock(true) гарантирует порядок, но снижает пропускную способность по сравнению со стандартным режимом.

4. Deadlock при неверном порядке захвата нескольких блокировок

ReentrantLock a = new ReentrantLock();
ReentrantLock b = new ReentrantLock();

// Поток 1: захват a затем b
// Поток 2: захват b затем a
// Возможна ситуация взаимной блокировки
Взаимная блокировка потоков; программа зависает

5. Ожидание без реакции на прерывания

При использовании lock() поток не реагирует на прерывания до получения блокировки; если требуется отменяемое ожидание, следует использовать lockInterruptibly() или tryLock с таймаутом.

Изменения в реализации и смежные релизы

API ReentrantLock присутствует с введения пакета java.util.concurrent (Java 5). Сигнатуры методов lock, lockInterruptibly, tryLock и unlock не претерпели существенных изменений в последних версиях JDK. В Java 8 и последующих релизах появились дополнительные высокопроизводительные альтернативы, например StampedLock. Внутренние оптимизации JVM могли менять производительность реализации, но совместимость API сохраняется.

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

Пример 1 - демонстрация реентрантности

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

public class ReentrantDemo {
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        lock.lock();
        try {
            System.out.println("Первый захват");
            nested();
        } finally { lock.unlock(); }
    }
    static void nested(){
        lock.lock(); // тот же поток может повторно захватить
        try { System.out.println("Второй захват"); }
        finally { lock.unlock(); }
    }
}
Первый захват
Второй захват

Пример 2 - использование Condition для очереди ожидания

Пример 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();
            }
            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 Exception {
        ConditionExample ex = new ConditionExample();
        Thread t = new Thread(() -> {
            try { ex.awaiter(); } catch (InterruptedException e) { }
        });
        t.start();
        Thread.sleep(100);
        ex.signaler();
        t.join();
    }
}
Продолжил после сигнала

Пример 3 - избегание deadlock с tryLock и таймаутом

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

public class DeadlockAvoid {
    static ReentrantLock a = new ReentrantLock();
    static ReentrantLock b = new ReentrantLock();

    static boolean tryAcquireBoth(ReentrantLock first, ReentrantLock second) throws InterruptedException {
        if (!first.tryLock(500, TimeUnit.MILLISECONDS)) return false;
        try {
            if (!second.tryLock(500, TimeUnit.MILLISECONDS)) return false;
            try { return true; } finally { second.unlock(); }
        } finally { first.unlock(); }
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            try { System.out.println(tryAcquireBoth(a, b)); } catch (InterruptedException e) {}
        });
        Thread t2 = new Thread(() -> {
            try { System.out.println(tryAcquireBoth(b, a)); } catch (InterruptedException e) {}
        });
        t1.start(); t2.start();
        t1.join(); t2.join();
    }
}
true
true

Пример 4 - прерываемое ожидание блокировки

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

public class InterruptibleDemo {
    public static void main(String[] args) throws Exception {
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        Thread t = new Thread(() -> {
            try {
                lock.lockInterruptibly();
                try { System.out.println("Получено"); } finally { lock.unlock(); }
            } catch (InterruptedException e) { System.out.println("Прервано во время ожидания"); }
        });
        t.start();
        Thread.sleep(100);
        t.interrupt();
        lock.unlock();
        t.join();
    }
}
Прервано во время ожидания

Пример 5 - справедливый и несчастливый режимы (наблюдение порядка)

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

public class FairVsUnfair {
    public static void main(String[] args) throws Exception {
        ReentrantLock fair = new ReentrantLock(true);
        ReentrantLock unfair = new ReentrantLock(false);
        // Поведение можно наблюдать запуская несколько потоков, которые пытаются захватить и печатать порядок.
        System.out.println("Справедливость: fair vs unfair описана в документации; в тестах fair даёт порядок очереди");
    }
}
Справедливость: fair vs unfair описана в документации; в тестах fair даёт порядок очереди

Приведённые примеры иллюстрируют общие и продвинутые сценарии: реентрантность, использование Condition, избежание deadlock с таймаутами и прерываемыми ожиданиями.

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

En
ReentrantLock.lock() Захватывает блокировку