Skip to content

同步与锁

概述

多线程同时访问共享变量时,若不做协调,会出现竞态条件(Race Condition):多个线程交错执行导致结果依赖执行顺序,从而产生错误或数据不一致。同步(Synchronization) 通过保证某段代码在同一时刻只被一个线程执行,从而得到正确、可预测的结果,即线程安全。本文介绍 Java 中最常用的两种同步方式:synchronized 关键字与 Lock 显式锁(以 ReentrantLock 为例),并说明使用场景与注意事项。学习前建议先掌握 线程创建与生命周期


为什么需要同步

当多个线程读写同一份数据时,若不加控制,会出现「读-改-写」被拆散、中间状态被其他线程修改等问题。

竞态条件示例

下面是一个典型的非线程安全示例:多个线程对同一计数器自增,由于自增不是原子操作,最终结果往往小于预期。

java
public class UnsafeCounter {
    private int count = 0;

    // 非线程安全:count++ 实际是「读-改-写」三步,可能被其他线程打断
    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        UnsafeCounter counter = new UnsafeCounter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) counter.increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) counter.increment();
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 期望 20000,实际往往小于 20000
        System.out.println("count = " + counter.getCount());
    }
}

通过同步,可以保证对 count 的「读-改-写」在获得锁的线程内完整执行,从而得到正确结果。


synchronized:内置锁

synchronized 是 Java 提供的内置同步机制,用法简单,由 JVM 负责加锁与释放。锁的是对象,同一时刻只有一个线程能持有该锁。

同步实例方法

在实例方法上加上 synchronized,锁的是当前实例对象this)。同一实例上的多个 synchronized 实例方法会互斥。

java
public class SafeCounter {
    private int count = 0;

    // 同步方法:同一时刻只有一个线程能执行该方法(针对同一 SafeCounter 实例)
    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

同步静态方法

在静态方法上加上 synchronized,锁的是当前类的 Class 对象。所有调用该静态方法的线程竞争的是同一把类锁。

java
public class StaticSyncDemo {
    private static int shared = 0;

    public static synchronized void add() {
        shared++;
    }

    public static synchronized int getShared() {
        return shared;
    }
}

同步块(指定锁对象)

若只想对方法内部分代码加锁,或希望锁的是指定对象,可使用同步块:synchronized (锁对象) { ... }。锁对象可以是任意对象,通常用专门用于加锁的 Object 或当前实例 this

java
public class SyncBlockDemo {
    private final Object lock = new Object();
    private int value = 0;

    public void update(int delta) {
        // 仅对共享变量的修改加锁,减少锁持有时间
        synchronized (lock) {
            value += delta;
        }
    }

    public int getValue() {
        synchronized (lock) {
            return value;
        }
    }
}

提示

同步块时,锁对象应使用 final 的成员变量或不可变对象,避免锁引用被替换导致不同线程使用不同的锁,失去同步效果。

示例:用 synchronized 修复计数器

java
public class SafeCounterDemo {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        SafeCounterDemo counter = new SafeCounterDemo();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) counter.increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) counter.increment();
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + counter.getCount()); // 稳定输出 20000
    }
}

Lock 显式锁(ReentrantLock)

从 JDK 1.5 开始,java.util.concurrent.locks 包提供了显式锁接口 Lock 及常用实现 ReentrantLock。与 synchronized 相比,显式锁需要手动 lock()unlock(),且 unlock() 必须放在 finally 中,否则锁可能无法释放。

基本用法

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

public class LockCounter {
    private final Lock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();  // 必须在 finally 中释放,否则异常时可能造成死锁
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

tryLock:非阻塞与超时

Lock 支持 tryLock():若当前锁被占用则立即返回 false,不阻塞;tryLock(long time, TimeUnit unit) 可限时等待。适合「尝试获取锁,拿不到就做别的事」的场景。

java
import java.util.concurrent.TimeUnit;

public class TryLockDemo {
    private final Lock lock = new ReentrantLock();

    public void tryDoSomething() {
        if (lock.tryLock()) {
            try {
                // 拿到锁,执行临界区代码
                System.out.println("获得锁,执行操作");
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("未获得锁,跳过或执行其他逻辑");
        }
    }

    public void tryLockWithTimeout() throws InterruptedException {
        if (lock.tryLock(2, TimeUnit.SECONDS)) {
            try {
                // 在 2 秒内拿到锁则执行
            } finally {
                lock.unlock();
            }
        }
    }
}

注意

InterruptedException 是受检异常。使用 tryLock(long, TimeUnit)lockInterruptibly() 时需处理该异常;在 catch 中通常应恢复中断状态:Thread.currentThread().interrupt()


synchronized 与 Lock 对比

特性synchronizedLock(如 ReentrantLock)
使用方式关键字,自动加锁/释放显式 lock() / unlock(),需在 finally 中 unlock
锁的获取拿不到就阻塞等待支持 tryLock、超时、可中断(lockInterruptibly)
公平性不保证公平ReentrantLock 可指定公平锁(fair=true)
适用场景简单临界区、代码简洁需要非阻塞、超时、多条件等复杂控制

提示

多数简单场景用 synchronized 即可,代码更短、不易漏释锁。需要「尝试获取」「超时」「公平锁」或与 Condition 配合时,再考虑 ReentrantLock


可重入性

可重入指同一线程可以多次获取同一把锁而不会死锁。Java 的 synchronized 与 ReentrantLock 都是可重入锁:若线程已持有锁,再次进入同一锁保护的代码时,只是增加持有计数,释放时逐层减少。

java
public class ReentrantDemo {
    public synchronized void a() {
        System.out.println("a");
        b();  // 同一线程再次进入 synchronized,不会阻塞自己
    }

    public synchronized void b() {
        System.out.println("b");
    }
}

注意事项

注意

  • 锁的对象要一致:多线程必须竞争同一把锁才能互斥。例如用 synchronized(lock) 时,所有线程必须使用同一个 lock 引用。
  • 避免死锁:多个线程互相等待对方持有的锁会死锁。尽量按固定顺序申请多把锁,或使用 tryLock 超时退出。

注意

使用 Lock 时,unlock() 必须放在 finally 中。若在 try 中 return 或抛异常而未 unlock,锁将无法释放,导致其他线程永久阻塞(逻辑上的死锁)。

提示

  • 缩小锁的粒度:只对共享数据的访问加锁,锁内尽量少做耗时或阻塞操作,减少竞争。
  • 不要锁住可变引用:synchronized(lock) 时,lock 不要换成别的对象;用 final 修饰锁引用可避免误改。

相关链接

基于 VitePress 构建