ReentrantLock în Java

Un lacăt: lock() lasă un singur fir să intre în zona protejată, unlock() îl eliberează; unlock se pune mereu în finally.

Sintaxă
import java.util.concurrent.locks.ReentrantLock;

ReentrantLock lacat = new ReentrantLock();
lacat.lock();
try {
    // cod pe care îl rulează un singur fir odată
} finally {
    lacat.unlock();
}

Ce face ReentrantLock

ReentrantLock protejează o bucată de cod (o „secțiune critică”) ca s-o ruleze un singur fir odată. lock() ia lacătul; dacă îl ține alt fir, așteaptă până se eliberează. unlock() eliberează lacătul pentru următorul fir.

Regula de aur: unlock() se scrie în blocul finally, imediat după lock(). Altfel, o excepție în zona protejată lasă lacătul blocat pentru totdeauna, iar celelalte fire așteaptă la nesfârșit.

„Reentrant” înseamnă că același fir poate lua lacătul de mai multe ori (de exemplu o metodă protejată apelează alta protejată de același lacăt); trebuie să-l elibereze de tot atâtea ori. Față de synchronized, oferă în plus tryLock, așteptare cu limită de timp și opțiunea de corectitudine (new ReentrantLock(true): firele intră în ordinea sosirii).

Ce returnează

lock() și unlock() nu returnează nimic. isLocked() și getHoldCount() dau starea lacătului.

Exemple

Exemplul 1: Un cont bancar folosit din mai multe fire

Patru fire fac câte 1000 de depuneri de 1 leu. Cu lacăt, soldul final este exact.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;

public class Main {
    static class Cont {
        private final ReentrantLock lacat = new ReentrantLock();
        private int sold = 0;

        void depune(int suma) {
            lacat.lock();
            try {
                sold = sold + suma;
            } finally {
                lacat.unlock();
            }
        }

        int sold() {
            lacat.lock();
            try {
                return sold;
            } finally {
                lacat.unlock();
            }
        }
    }

    public static void main(String[] args) {
        Cont cont = new Cont();
        try (ExecutorService executor = Executors.newFixedThreadPool(4)) {
            for (int i = 0; i < 4; i++) {
                executor.submit(() -> {
                    for (int j = 0; j < 1000; j++) cont.depune(1);
                });
            }
        }
        System.out.println("Sold final: " + cont.sold() + " lei");
    }
}

Rezultat

Sold final: 4000 lei

Exemplul 2: Același fir ia lacătul de două ori

import java.util.concurrent.locks.ReentrantLock;

public class Main {
    static final ReentrantLock LACAT = new ReentrantLock();

    static void salveaza() {
        LACAT.lock();
        try {
            System.out.println("salveaza: getHoldCount = " + LACAT.getHoldCount());
            valideaza();
        } finally {
            LACAT.unlock();
        }
    }

    static void valideaza() {
        LACAT.lock();
        try {
            System.out.println("valideaza: getHoldCount = " + LACAT.getHoldCount());
        } finally {
            LACAT.unlock();
        }
    }

    public static void main(String[] args) {
        salveaza();
        System.out.println("La final, blocat: " + LACAT.isLocked());
    }
}

Rezultat

salveaza: getHoldCount = 1
valideaza: getHoldCount = 2
La final, blocat: false

Exemplul 3: unlock în finally, chiar dacă apare o excepție

import java.util.concurrent.locks.ReentrantLock;

public class Main {
    public static void main(String[] args) {
        ReentrantLock lacat = new ReentrantLock();
        try {
            lacat.lock();
            try {
                Integer.parseInt("o sută");
            } finally {
                lacat.unlock();
            }
        } catch (NumberFormatException e) {
            System.out.println("Eroare: " + e.getMessage());
        }
        System.out.println("Lacătul a rămas blocat? " + lacat.isLocked());
    }
}

Rezultat

Eroare: For input string: "o sută"
Lacătul a rămas blocat? false

Greșeli frecvente

  • Pui unlock() la sfârșitul blocului, nu în finally. La prima excepție lacătul rămâne luat și programul se blochează.
  • Apelezi unlock() dintr-un fir care nu ține lacătul. Aruncă IllegalMonitorStateException.
  • Pui lock() în interiorul lui try. Dacă lock() eșuează, finally apelează unlock() fără lacăt; pune lock() chiar înainte de try.

Comenzi înrudite

  • ReentrantLock.tryLock: Încearcă să ia lacătul fără să aștepte la nesfârșit: returnează true dacă l-a luat, false dacă este ocupat.
  • AtomicInteger: Un int pe care mai multe fire îl pot modifica sigur, fără synchronized: get(), set(), addAndGet() și altele.
  • ConcurrentHashMap: Un Map pe care mai multe fire îl pot citi și modifica în același timp, fără să se strice datele.
  • CountDownLatch: Un numărător pentru așteptare: await() blochează până când alte fire apelează countDown() de N ori.