synchronized în Java

synchronized lasă un singur fir de execuție odată într-o metodă sau un bloc, ca datele comune să nu fie corupte.

Sintaxă
synchronized void incrementeaza() { ... }   // metodă: blochează pe this

synchronized (blocare) {                     // bloc: blochează pe obiectul dat
    // un singur fir la un moment dat
}

Ce face synchronized

Când mai multe fire de execuție modifică aceeași variabilă, operațiile se pot amesteca. contor++ înseamnă de fapt trei pași (citește, adună, scrie), iar două fire pot citi aceeași valoare și pierde o incrementare (race condition).

Fiecare obiect Java are un „lacăt” (monitor). O metodă synchronized ia lacătul lui this (sau al clasei, pentru metode static), iar un bloc synchronized (obiect) { ... } ia lacătul obiectului dat. Cât timp un fir ține lacătul, celelalte fire care vor același lacăt așteaptă. La ieșire, modificările devin vizibile pentru următorul fir.

Ține blocurile sincronizate cât mai scurte. Pentru cazuri simple există alternative mai rapide: AtomicInteger, ConcurrentHashMap și lacătele din java.util.concurrent.locks.

Exemple

Exemplul 1: Contor partajat, cu synchronized

Patru fire incrementează de 100.000 de ori același contor. Pentru că incrementeaza este synchronized, rezultatul este mereu 400.000. join() așteaptă terminarea firelor înainte de afișare.

class Contor {
    private int valoare;

    synchronized void incrementeaza() {
        valoare++;
    }

    synchronized int valoare() {
        return valoare;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Contor vizitatori = new Contor();
        Thread[] fire = new Thread[4];
        for (int i = 0; i < fire.length; i++) {
            fire[i] = new Thread(() -> {
                for (int j = 0; j < 100_000; j++) {
                    vizitatori.incrementeaza();
                }
            });
            fire[i].start();
        }
        for (Thread t : fire) {
            t.join();
        }
        System.out.println("vizitatori: " + vizitatori.valoare());
    }
}

Rezultat

vizitatori: 400000

Exemplul 2: Fără synchronized: incrementări pierdute

Același program, dar fără synchronized. Rezultatul este de obicei mai mic de 400.000 și diferă la fiecare rulare. Uneori iese chiar corect, și tocmai de aceea astfel de bug-uri sunt greu de prins.

public class Main {
    static int contor = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread[] fire = new Thread[4];
        for (int i = 0; i < fire.length; i++) {
            fire[i] = new Thread(() -> {
                for (int j = 0; j < 100_000; j++) {
                    contor++;          // nesincronizat
                }
            });
            fire[i].start();
        }
        for (Thread t : fire) {
            t.join();
        }
        System.out.println("contor: " + contor + " (așteptat 400000)");
    }
}

Rezultat posibil (diferă la fiecare rulare)

contor: 352122 (așteptat 400000)

Exemplul 3: Bloc synchronized pe un obiect de blocare

Doar transferul (retragere + depunere) este protejat, cu un obiect dedicat blocare. Suma totală din cele două conturi rămâne constantă.

public class Main {
    private static final Object blocare = new Object();
    private static int contA = 20_000;
    private static int contB = 20_000;

    static void transfera(int suma, boolean dinA) {
        synchronized (blocare) {
            if (dinA) {
                contA -= suma;
                contB += suma;
            } else {
                contB -= suma;
                contA += suma;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) transfera(3, true); });
        Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) transfera(2, false); });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("A = " + contA + ", B = " + contB + ", total = " + (contA + contB));
    }
}

Rezultat

A = 15000, B = 25000, total = 40000

Greșeli frecvente

  • Sincronizezi pe obiecte diferite (fiecare fir pe propriul this) și crezi că ești protejat. Toate firele trebuie să folosească același lacăt.
  • Sincronizezi scrierea, dar nu și citirea: un fir poate vedea o valoare veche. Ambele accesări trebuie să folosească același lacăt (sau volatile/clase atomice).
  • Două fire iau două lacăte în ordine inversă și se blochează reciproc pentru totdeauna (deadlock). Ia lacătele mereu în aceeași ordine.
  • Faci sincronizat un bloc mare care conține operații lente (rețea, fișiere): firele așteaptă degeaba.

Comenzi înrudite

  • volatile: volatile pe un câmp garantează că fiecare fir vede imediat ultima valoare scrisă; nu face însă operațiile compuse (x++) atomice.
  • Fire de execuție virtuale: Firele virtuale sunt fire de execuție foarte ușoare, gestionate de JVM: poți porni zeci de mii de sarcini care așteaptă fără să blochezi sistemul.
  • Thread: Un fir de execuție: cod care rulează în paralel cu restul programului. start() îl pornește, join() așteaptă să se termine.
  • static: static leagă un câmp sau o metodă de clasă, nu de un obiect: există o singură copie, accesată prin numele clasei.