一聚教程网:一个值得你收藏的教程网站

热门教程

Java ReentrantLock 实战详细说明:比 synchronized 更灵活的可重入锁

时间:2026-08-03 10:18:49 编辑:袖梨 来源:一聚教程网

Java ReentrantLock 实战详细说明:比 synchronized 更灵活的可重入锁并不只看表面做法,关键还要理解相关条件、限制和后续影响。

简介

ReentrantLock 是 Java 并发包 java.util.concurrent.locks 里的可重入互斥锁。

Java ReentrantLock 实战详解:比 synchronized 更灵活的可重入锁

它和 synchronized 一样,都能解决多线程同时修改共享数据带来的线程安全问题。

一句话概括:

 复制代码ReentrantLock 是一把需要手动加锁、手动释放的可重入锁,比 synchronized 多了超时获取、可中断等待、公平锁和多个 Condition 等能力。

典型写法:

 复制代码lock.lock();try {// 临界区代码} finally {    lock.unlock();}

finally 很关键。

只要拿到了锁,就必须保证最后释放。否则业务代码中途抛异常后,锁一直不释放,后面的线程会全部卡住。

为什么需要 ReentrantLock

先看 synchronized

 复制代码public synchronizedvoidincrement() {    count++;}

这种写法简单直接。

但它有几个限制:

  1. 等锁时不能设置超时时间
  2. 等锁时不方便响应中断
  3. 不能选择公平锁
  4. 只能配合一个对象监视器等待队列
  5. 不能像 API 一样灵活组合

ReentrantLock 提供了更多控制能力:

能力说明
lock()阻塞获取锁
tryLock()立即尝试获取锁,拿不到直接返回
tryLock(timeout, unit)最多等待一段时间
lockInterruptibly()等锁时可以响应中断
newCondition()创建多个条件队列
公平锁可按等待顺序分配锁
查询方法查看锁状态、等待线程估算数量、重入次数

所以,简单同步用 synchronized 就够。

需要更强控制时,再考虑 ReentrantLock

可重入是什么意思

ReentrantLock 里的 Reentrant 表示可重入。

可重入的意思是:

 复制代码同一个线程已经拿到锁后,可以再次拿同一把锁,不会把自己锁死。

示例:

 复制代码import java.util.concurrent.locks.ReentrantLock;public classReentrantDemo {private finalReentrantLocklock=newReentrantLock();public voidouter() {        lock.lock();        try {            System.out.println("outer holdCount=" + lock.getHoldCount());            inner();        } finally {            lock.unlock();            System.out.println("outer unlock");        }    }    private voidinner() {        lock.lock();        try {            System.out.println("inner holdCount=" + lock.getHoldCount());        } finally {            lock.unlock();            System.out.println("inner unlock");        }    }    public staticvoidmain(String[] args) {new ReentrantDemo().outer();    }}

输出类似:

 复制代码outer holdCount=1inner holdCount=2inner unlockouter unlock

注意:

 复制代码加锁几次,就要释放几次。

同一个线程加了 2 次锁,只释放 1 次,锁仍然没有完全释放。

ReentrantLock 和 synchronized 对比

对比项synchronizedReentrantLock
类型Java 关键字JUC API
释放锁自动释放必须手动 unlock()
可重入支持支持
公平锁不支持显式选择构造函数可指定
尝试获取不支持tryLock()
超时获取不支持tryLock(time, unit)
可中断等待不支持lockInterruptibly()
条件队列wait/notify 一个等待集合可创建多个 Condition
可读性简单场景更清楚复杂场景更灵活
性能现代 JVM 优化后很好和场景有关,不应只为性能替换

常见选择:

 复制代码简单互斥:synchronized需要超时、取消、公平、多条件队列:ReentrantLock读多写少:ReentrantReadWriteLock 或 StampedLock限制并发数量:Semaphore

构造方法

默认构造:

 复制代码ReentrantLock lock=newReentrantLock();

等价于:

 复制代码ReentrantLock lock=newReentrantLock(false);

也就是非公平锁。

公平锁:

 复制代码ReentrantLock fairLock=newReentrantLock(true);

两者区别:

说明
非公平锁新来的线程可以直接竞争锁,吞吐量通常更高
公平锁倾向于让等待时间最长的线程先拿锁,减少饥饿

公平锁不是绝对严格的全局公平。

官方文档里有一个容易忽略的细节:即使创建了公平锁,普通 tryLock() 只要看到锁空闲,也会立即尝试获取,可能不遵守公平排队。如果希望尽量遵守公平策略,可以使用 tryLock(0, TimeUnit.SECONDS)

常用方法

方法作用
lock()获取锁,拿不到就一直等
lockInterruptibly()获取锁,等待期间可响应中断
tryLock()立即尝试获取锁,成功返回 true
tryLock(time, unit)在指定时间内尝试获取锁
unlock()释放锁
newCondition()创建条件变量
isLocked()锁是否被任意线程持有
isHeldByCurrentThread()当前线程是否持有锁
getHoldCount()当前线程重入次数
isFair()是否公平锁
hasQueuedThreads()是否有线程在等待锁
getQueueLength()估算等待锁的线程数量

这些状态查询方法适合监控和排查问题。

业务逻辑不要强依赖 getQueueLength() 这类估算结果做精确判断。

第一个 Demo:计数器线程安全

count++ 不是原子操作。

它大致包含:

 复制代码读取 count加 1写回 count

多个线程同时执行时,结果可能丢失。

不加锁示例:

 复制代码public classUnsafeCounter {private int count;public voidincrement() {        count++;    }    public intgetCount() {return count;    }}

使用 ReentrantLock

 复制代码import java.util.ArrayList;import java.util.List;import java.util.concurrent.locks.ReentrantLock;public classSafeCounterDemo {static classSafeCounter {private finalReentrantLocklock=newReentrantLock();private int count;public voidincrement() {            lock.lock();            try {                count++;            } finally {                lock.unlock();            }        }        public intgetCount() {            lock.lock();            try {return count;            } finally {                lock.unlock();            }        }    }    public staticvoidmain(String[] args)throws InterruptedException {SafeCounter counter=newSafeCounter();        List<Thread> threads = new ArrayList<>();for (inti=0; i < 100; i++) {Thread thread=newThread(() -> {for (intj=0; j < 1000; j++) {                    counter.increment();                }            });            threads.add(thread);            thread.start();        }        for (Thread thread : threads) {            thread.join();        }        System.out.println(counter.getCount());    }}

输出:

 复制代码100000

为什么 unlock 必须放 finally

错误写法:

 复制代码lock.lock();doBusiness();lock.unlock();

如果 doBusiness() 抛异常,unlock() 不会执行。

正确写法:

 复制代码lock.lock();try {    doBusiness();} finally {    lock.unlock();}

如果代码里存在多个返回分支,也一样:

 复制代码lock.lock();try {if (condition) {return;    }    doBusiness();} finally {    lock.unlock();}

finally 会保证方法返回前释放锁。

tryLock:拿不到锁就不等

tryLock() 适合“能做就做,不能做就跳过”的场景。

比如后台刷新缓存,没必要多个线程一起排队刷新。

 复制代码import java.util.concurrent.locks.ReentrantLock;public classCacheRefreshService {private finalReentrantLockrefreshLock=newReentrantLock();public voidrefresh() {if (!refreshLock.tryLock()) {            System.out.println("已有线程在刷新缓存,本次跳过");return;        }        try {            System.out.println("开始刷新缓存");            sleep(1000);            System.out.println("缓存刷新完成");        } finally {            refreshLock.unlock();        }    }    private voidsleep(long millis) {try {            Thread.sleep(millis);        } catch (InterruptedException e) {            Thread.currentThread().interrupt();        }    }}

tryLock() 不会阻塞等待。

拿不到锁就返回 false

tryLock 超时:最多等一会

有些业务不能无限等锁。

比如库存扣减、订单状态更新、用户提交请求。

等太久不如直接返回失败。

 复制代码import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.ReentrantLock;public classStockService {private finalReentrantLocklock=newReentrantLock();private intstock=10;public booleandeduct(int amount) {boolean locked=false;try {            locked = lock.tryLock(500, TimeUnit.MILLISECONDS);if (!locked) {                System.out.println("系统繁忙,稍后重试");return false;            }            if (stock < amount) {                System.out.println("库存不足");return false;            }            stock -= amount;            System.out.println("扣减成功,剩余库存:" + stock);return true;        } catch (InterruptedException e) {            Thread.currentThread().interrupt();            System.out.println("等待锁时被中断");return false;        } finally {if (locked) {                lock.unlock();            }        }    }}

这里要注意:

 复制代码if (locked) {    lock.unlock();}

只有成功拿到锁,才能释放锁。

没拿到锁却调用 unlock(),会抛 IllegalMonitorStateException

lockInterruptibly:等待锁时可以取消

lock() 等待锁时不会因为普通中断直接退出。

lockInterruptibly() 可以在等待锁时响应中断。

适合可取消任务、后台任务停止、避免线程长期挂起。

 复制代码import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.ReentrantLock;public classInterruptibleLockDemo {private finalReentrantLocklock=newReentrantLock();public voidlongTask() {        lock.lock();        try {            System.out.println("线程 A 持有锁");            sleep(5000);        } finally {            lock.unlock();        }    }    public voidcancelableTask() {boolean locked=false;try {            lock.lockInterruptibly();            locked = true;            System.out.println("线程 B 获取锁成功");        } catch (InterruptedException e) {            Thread.currentThread().interrupt();            System.out.println("线程 B 等锁时被中断,直接退出");        } finally {if (locked) {                lock.unlock();            }        }    }    public staticvoidmain(String[] args)throws InterruptedException {InterruptibleLockDemo demo=newInterruptibleLockDemo();Thread threadA=newThread(demo::longTask, "A");Thread threadB=newThread(demo::cancelableTask, "B");        threadA.start();        TimeUnit.MILLISECONDS.sleep(100);        threadB.start();        TimeUnit.SECONDS.sleep(1);        threadB.interrupt();        threadA.join();        threadB.join();    }    private voidsleep(long millis) {try {            Thread.sleep(millis);        } catch (InterruptedException e) {            Thread.currentThread().interrupt();        }    }}

输出类似:

 复制代码线程 A 持有锁线程 B 等锁时被中断,直接退出

Condition 是什么

Condition 可以理解成和 ReentrantLock 绑定的等待队列。

它类似 Object.wait() / notify(),但更灵活。

synchronized 只有一个等待集合:

 复制代码同一把锁  |  v一个 wait 队列

ReentrantLock 可以创建多个 Condition

 复制代码同一把锁  |  +--> notFull 队列  |  +--> notEmpty 队列

这样生产者只唤醒消费者,消费者只唤醒生产者,逻辑更清楚。

常用方法:

方法作用
await()当前线程等待,并释放锁
await(timeout, unit)限时等待
signal()唤醒一个等待线程
signalAll()唤醒所有等待线程

await() 有两个关键点:

 复制代码调用 await 时会释放当前锁。被唤醒返回前,需要重新获取同一把锁。

Condition Demo:有界队列

下面实现一个简单有界队列。

队列满时,生产者等待。

队列空时,消费者等待。

 复制代码import java.util.ArrayDeque;import java.util.Queue;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.ReentrantLock;public classBoundedQueue<T> {private final Queue<T> queue = newArrayDeque<>();private finalint capacity;private finalReentrantLocklock=newReentrantLock();private finalConditionnotFull= lock.newCondition();private finalConditionnotEmpty= lock.newCondition();public BoundedQueue(int capacity) {if (capacity <= 0) {throw newIllegalArgumentException("capacity must be positive");        }        this.capacity = capacity;    }    public voidput(T item)throws InterruptedException {        lock.lock();        try {while (queue.size() == capacity) {                notFull.await();            }            queue.offer(item);            notEmpty.signal();        } finally {            lock.unlock();        }    }    public T take()throws InterruptedException {        lock.lock();        try {while (queue.isEmpty()) {                notEmpty.await();            }            T item= queue.poll();            notFull.signal();            return item;        } finally {            lock.unlock();        }    }    public intsize() {        lock.lock();        try {return queue.size();        } finally {            lock.unlock();        }    }}

测试:

 复制代码public classBoundedQueueDemo {public staticvoidmain(String[] args) {        BoundedQueue<Integer> queue = new BoundedQueue<>(3);Thread producer=newThread(() -> {try {for (inti=1; i <= 10; i++) {                    queue.put(i);                    System.out.println("生产:" + i + ",size=" + queue.size());                    Thread.sleep(100);                }            } catch (InterruptedException e) {                Thread.currentThread().interrupt();            }        }, "producer");Thread consumer=newThread(() -> {try {for (inti=1; i <= 10; i++) {Integer item= queue.take();                    System.out.println("消费:" + item + ",size=" + queue.size());                    Thread.sleep(300);                }            } catch (InterruptedException e) {                Thread.currentThread().interrupt();            }        }, "consumer");        producer.start();        consumer.start();    }}

这里必须用 while 判断条件,不能用 if

原因有两个:

  1. 线程被唤醒后,不代表条件一定满足
  2. Condition 允许虚假唤醒

正确写法:

 复制代码while (queue.isEmpty()) {    notEmpty.await();}

不要写:

 复制代码if (queue.isEmpty()) {    notEmpty.await();}

signal 和 signalAll 怎么选

signal() 唤醒一个等待线程。

signalAll() 唤醒所有等待线程。

选择建议:

方法适合场景
signal()每次状态变化只够一个线程继续执行
signalAll()状态变化可能让多个线程继续执行,或条件复杂

有界队列里:

 复制代码notEmpty.signal();notFull.signal();

通常够用。

如果业务条件很多、容易漏唤醒,先用 signalAll() 保守一点,再结合性能情况优化。

公平锁 Demo

公平锁会尽量按等待顺序分配锁。

非公平锁可能让新来的线程插队。

 复制代码import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.ReentrantLock;public classFairLockDemo {private finalReentrantLocklock=newReentrantLock(true);public voidwork() {        lock.lock();        try {            System.out.println(Thread.currentThread().getName() + " 获取锁");            TimeUnit.MILLISECONDS.sleep(200);        } catch (InterruptedException e) {            Thread.currentThread().interrupt();        } finally {            lock.unlock();        }    }    public staticvoidmain(String[] args) {FairLockDemo demo=newFairLockDemo();for (inti=0; i < 5; i++) {new Thread(demo::work, "thread-" + i).start();        }    }}

公平锁的代价是吞吐量通常更低。

原因是它更强调排队顺序,会带来更多线程调度和上下文切换。

大多数业务优先使用默认非公平锁。

只有明确需要减少线程饥饿或顺序性时,再考虑公平锁。

实战 Demo:转账避免死锁

转账经常需要同时锁两个账户。

如果两个线程这样执行:

 复制代码线程 1:锁 A -> 等 B线程 2:锁 B -> 等 A

就可能死锁。

解决方式之一:按固定顺序加锁。

 复制代码import java.math.BigDecimal;import java.util.concurrent.locks.ReentrantLock;public classAccount {private finallong id;private BigDecimal balance;private finalReentrantLocklock=newReentrantLock();public Account(long id, BigDecimal balance) {this.id = id;this.balance = balance;    }    public staticbooleantransfer(Account from, Account to, BigDecimal amount) {Account first= from.id < to.id ? from : to;Account second= from.id < to.id ? to : from;        first.lock.lock();        try {            second.lock.lock();            try {if (from.balance.compareTo(amount) < 0) {return false;                }                from.balance = from.balance.subtract(amount);                to.balance = to.balance.add(amount);                return true;            } finally {                second.lock.unlock();            }        } finally {            first.lock.unlock();        }    }    public BigDecimal getBalance() {        lock.lock();        try {return balance;        } finally {            lock.unlock();        }    }}

关键点:

 复制代码所有线程都按账户 id 从小到大加锁。

只要加锁顺序统一,就不会出现 A 等 B、B 等 A 的环路。

另一种方式是用 tryLock(timeout),拿不到第二把锁就释放第一把锁并返回失败。

适合不想长期等待的业务。

实战 Demo:转账超时失败

 复制代码import java.math.BigDecimal;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.ReentrantLock;public classTimeoutTransferService {static classAccount {private final String name;private BigDecimal balance;private finalReentrantLocklock=newReentrantLock();        Account(String name, BigDecimal balance) {            this.name = name;this.balance = balance;        }    }    public booleantransfer(Account from, Account to, BigDecimal amount) {boolean fromLocked=false;boolean toLocked=false;try {            fromLocked = from.lock.tryLock(300, TimeUnit.MILLISECONDS);if (!fromLocked) {return false;            }            toLocked = to.lock.tryLock(300, TimeUnit.MILLISECONDS);if (!toLocked) {return false;            }            if (from.balance.compareTo(amount) < 0) {return false;            }            from.balance = from.balance.subtract(amount);            to.balance = to.balance.add(amount);            return true;        } catch (InterruptedException e) {            Thread.currentThread().interrupt();            return false;        } finally {if (toLocked) {                to.lock.unlock();            }            if (fromLocked) {                from.lock.unlock();            }        }    }}

这种写法的重点是:

 复制代码分别记录每把锁是否获取成功。finally 里只释放已经拿到的锁。释放顺序和加锁顺序相反。

ReentrantLock 底层和 AQS

ReentrantLock 底层基于 AbstractQueuedSynchronizer,简称 AQS

简单理解:

 复制代码AQS 用一个 int state 表示同步状态用队列管理等待线程ReentrantLock 用 state 表示锁重入次数

大致关系:

 复制代码state = 0:锁空闲state = 1:当前线程持有锁 1 次state = 2:当前线程重入 2 次

线程获取不到锁时,会进入 AQS 等待队列。

释放锁时,state 减 1。

只有减到 0,锁才真正释放,后面的线程才有机会获取。

这也是“加几次锁,就要解几次锁”的底层原因。

和虚拟线程的关系

JDK 21 之后,虚拟线程已经正式可用。

在虚拟线程里使用 ReentrantLock 是可以的。

示例:

 复制代码import java.util.concurrent.Executors;import java.util.concurrent.locks.ReentrantLock;public classVirtualThreadLockDemo {private staticfinalReentrantLocklock=newReentrantLock();private staticint count;public staticvoidmain(String[] args) {try (varexecutor= Executors.newVirtualThreadPerTaskExecutor()) {for (inti=0; i < 1000; i++) {                executor.submit(() -> {                    lock.lock();                    try {                        count++;                    } finally {                        lock.unlock();                    }                });            }        }        System.out.println(count);    }}

但锁竞争本身不会因为虚拟线程消失。

如果所有虚拟线程都抢同一把锁,临界区仍然只能一个线程执行。

虚拟线程降低的是线程成本,不是把串行临界区变成并行。

常见坑

忘记 unlock

最严重的错误:

 复制代码lock.lock();doBusiness();

没有 unlock(),其他线程会一直等。

标准写法:

 复制代码lock.lock();try {    doBusiness();} finally {    lock.unlock();}

没拿到锁也 unlock

错误写法:

 复制代码try {    lock.tryLock(1, TimeUnit.SECONDS);    doBusiness();} finally {    lock.unlock();}

如果 tryLock 返回 false,这里会释放一把并未持有的锁,抛 IllegalMonitorStateException

正确写法:

 复制代码boolean locked=false;try {    locked = lock.tryLock(1, TimeUnit.SECONDS);if (!locked) {return;    }    doBusiness();} finally {if (locked) {        lock.unlock();    }}

Condition 用 if 判断

错误写法:

 复制代码if (queue.isEmpty()) {    notEmpty.await();}

正确写法:

 复制代码while (queue.isEmpty()) {    notEmpty.await();}

原因是线程醒来后必须重新检查条件。

await / signal 时没有持有锁

Conditionawait()signal() 通常要求当前线程已经持有对应的锁。

错误写法:

 复制代码notEmpty.signal();

正确写法:

 复制代码lock.lock();try {    notEmpty.signal();} finally {    lock.unlock();}

否则可能抛 IllegalMonitorStateException

公平锁当成性能优化

公平锁主要解决等待顺序和饥饿问题。

它通常不是性能优化手段。

高吞吐场景一般优先默认非公平锁。

锁粒度太大

锁保护的代码越多,竞争越严重。

不推荐:

 复制代码lock.lock();try {    queryDatabase();    callRemoteApi();    updateMemoryState();} finally {    lock.unlock();}

更好的方向是缩小临界区:

 复制代码锁内只保护必须同步的共享状态。耗时 I/O 尽量放到锁外。

能用原子类却用了锁

简单计数可以用:

 复制代码AtomicIntegerLongAdder

比如高并发统计计数,LongAdder 往往比一把锁更合适。

ReentrantLock 更适合保护一段复合逻辑,而不只是单个自增。

总结

ReentrantLock 可以按这条线理解:

 复制代码它是一把可重入互斥锁。同一线程可以重复加锁。每次加锁都要对应一次释放。tryLock 支持不等待或限时等待。lockInterruptibly 支持等待时取消。Condition 支持多个等待队列。公平锁可以减少饥饿,但通常牺牲吞吐量。

实际使用时,优先记住这些规则:

  1. lock() 后一定 try-finally-unlock
  2. tryLock() 后只在成功获取锁时释放
  3. Condition.await() 放在 while
  4. await()signal() 必须在持锁状态下调用
  5. 公平锁慎用
  6. 临界区尽量短
  7. 简单同步优先考虑 synchronized
  8. 简单计数优先考虑原子类

ReentrantLock 的价值不在于替代所有 synchronized,而是在复杂并发控制里提供更细的操作空间。需要超时、取消、公平、多条件等待时,它比内置锁更合适;只是普通互斥时,synchronized 往往更简单,也更不容易忘记释放锁。

热门栏目