当前位置: 代码网 > it编程>编程语言>Java > Java 并发编程ReentrantLock 公平锁与非公平锁深度解析

Java 并发编程ReentrantLock 公平锁与非公平锁深度解析

2026年08月21日 Java 我要评论
前言在 java 并发编程中,reentrantlock 是 synchronized 关键字之外最常用的互斥锁实现。相比 synchronized,它更加灵活:可以手动加锁解锁、尝试加锁、响应中断,

前言

在 java 并发编程中,reentrantlock 是 synchronized 关键字之外最常用的互斥锁实现。

相比 synchronized,它更加灵活:可以手动加锁解锁尝试加锁响应中断

还可以选择公平模式非公平模式

但很多开发者在使用时只写 new reentrantlock(),没有仔细想过 "非公平" 意味着什么,

以及什么时候必须用公平锁,什么时候应该用非公平锁。

本文从原理、应用场景、性能对比、选型指南四个维度,带你彻底吃透这对概念。

一、什么是 reentrantlock

java.util.concurrent.locks.reentrantlock 是 jdk 提供的可重入互斥锁。

它的核心特性:

  • 可重入:同一个线程可以多次获得同一把锁(计数 +1),必须释放相同次数
  • 可中断:支持 lockinterruptibly(),等待锁的线程可以被中断
  • 尝试加锁:支持 trylock() / trylock(timeout),拿不到就返回
  • 公平 / 非公平:构造参数决定排队策略
// 默认非公平
reentrantlock lock1 = new reentrantlock();
// 显式非公平
reentrantlock lock2 = new reentrantlock(false);
// 公平锁
reentrantlock lock3 = new reentrantlock(true);

它的内部维护一个等待队列,线程来抢锁时根据 "公平 / 非公平" 规则决定谁先拿到。

二、非公平锁(nonfair sync)

2.1 工作方式

不排队,谁抢到算谁。

当一个线程尝试获取锁时:

  • 先尝试一次 cas 抢锁
  • 抢到就执行临界区
  • 抢不到才进入等待队列尾部
注意:并不是锁释放的方法内部主动给新线程插队机会;
当锁释放 state 置为 0,队列中的等待线程还未被唤醒时,如果新线程恰好执行 lock (),会直接 cas 抢占 state,插队成功;队列里已经排队的线程继续阻塞等待。

2.2 例子

假设 .smwu 锁被线程 a 持有,b、c、d 已经在等待队列里。

时刻 1:a 持有锁,等待队列: b -> c -> d
时刻 2:a 释放锁的瞬间,新线程 e 也来抢锁

非公平锁下的执行顺序

 ┌─ e 直接插队抢锁 ─┐
         │                 ▼
释放锁 → 队列头: b  c  d   ←  e 抢到了
         │
         └─ 队列里 b/c/d 还在等

e 虽然后到,但因为 cas 抢锁成功,b 反而被插队

2.3 优点

  • 吞吐量大:减少线程切换开销
  • 适合锁持有时间极短的场景(cas 成功率很高)

2.4 缺点

  • 可能 "饿死":b 如果运气差,被 e、f、g 一直插队,永远轮不到
  • 不保证 fifo

三、公平锁(fair sync)

3.1 工作方式

严格按申请顺序排队(fifo),新来的线程不能插队,必须排到队尾。

当一个线程尝试获取锁时:

  • 通过 hasqueuedpredecessors() 判断队列是否存在排在自己前面的等待线程
  • 如果存在前驱等待线程,直接进入队列尾部排队;只有无前驱时,才允许尝试 cas 获取锁
  • 锁释放时,唤醒队列头部的线程

边界:可重入场景,如果当前线程本身就是队列头节点,允许直接获取锁,不需要排队。

3.2 同样场景

时刻 1:a 持有锁,等待队列: b -> c -> d
时刻 2:a 释放锁的瞬间,新线程 e 也来抢锁

公平锁下的执行顺序

释放锁 → 锁交给 b(b 先到,理所当然)
         队列变成: c -> d -> e

e 只能乖乖排到队尾,不能插队。

3.3 优点

  • 不会饿死:先到的一定先服务,fifo 极大降低饥饿概率
  • 可预测:知道每个线程大致的等待时间

补充:公平锁是应用层 fifo 排队,受操作系统线程调度优先级影响,不能做到绝对时序保障

3.4 缺点

  • 吞吐量略低:每次释放锁都要唤醒队头,带来一定线程切换成本
  • 但在 "锁持有时间较长" 的场景下,这点开销基本可以忽略

四、原理对比

4.1 aqs 队列

reentrantlock 内部基于 abstractqueuedsynchronizer(aqs)实现。

aqs 维护一个变体 clh 双向阻塞队列;原版 clh 为自旋锁,aqs 使用 locksupport.park() 实现线程阻塞,队列中每个 node 节点代表一个等待线程。

  • 非公平锁:线程先尝试 cas 修改 state,失败才入队
  • 公平锁:线程先判断是否存在前驱等待节点,有就直接入队

4.2 源码核心差异

// 非公平 lock
final void lock() {
    if (compareandsetstate(0, 1))
        setexclusiveownerthread(thread.currentthread());
    else
        acquire(1);
}
// 公平 lock
final void lock() {
    acquire(1);
}
// 公平 tryacquire
protected final boolean tryacquire(int acquires) {
    final thread current = thread.currentthread();
    int c = getstate();
    if (c == 0) {
        if (!hasqueuedpredecessors() &&  // 关键:有前驱等待线程就放弃抢锁
            compareandsetstate(0, acquires)) {
            setexclusiveownerthread(current);
            return true;
        }
    }
    // ... 可重入逻辑
}

关键差异就在 hasqueuedpredecessors()

  • 非公平:直接 cas 抢,不管队列
  • 公平:发现队列里有排在自己前面的线程,就老老实实排队

⚠️重要隐藏坑:公平锁实例调用无参 trylock(),会无视公平策略直接 cas 抢锁,可以插队!

  • lock() / lockinterruptibly() / trylock(timeout, unit):遵守公平规则
  • trylock()(无参):无论公平 / 非公平实例,直接抢锁,不做排队判断
reentrantlock fairlock = new reentrantlock(true);
fairlock.trylock();                 // ❗无视公平策略,可插队
fairlock.trylock(5, timeunit.seconds); // ✅遵守公平排队
fairlock.lock();                    // ✅遵守公平排队

五、性能对比

很多人有个误解:"公平锁一定比非公平慢很多"。其实不一定。

5.1 锁持有时间极短时(ns 级)

非公平锁:吞吐量 100%
公平锁  :吞吐量 70% 左右
差异明显,因为唤醒队头的开销占大头。

5.2 锁持有时间较长时(ms/s 级)

非公平锁:吞吐量 100%
公平锁  :吞吐量 95% 左右
差异很小,因为唤醒队头的开销相对临界区可忽略。

5.3 性能压测参考

注:压测数据为理论参考值,不同 cpu、jdk 版本实际 qps 会浮动。

临界区耗时线程数公平锁 qps非公平锁 qps差异
100ns16800 万1100 万~30%
1μs1690 万110 万~20%
10μs169 万10 万~10%
100μs1690009500~5%
1ms16900920~2%
10ms+16~ 持平~ 持平<1%

结论:临界区越长,公平锁的性能损失越小。

六、应用场景

6.1 选非公平锁的场景

特点:锁持有时间极短,争用激烈但单次持有时间可以忽略。

场景 a:内存计数器

private final reentrantlock lock = new reentrantlock();
public void increment() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock();
    }
}
  • count++ 一行纳秒级操作
  • 唤醒队头线程的开销比这次操作本身还大
  • 谁先抢到无所谓,反正所有线程都加一次

场景 b:缓存击穿保护

public object get(string key) {
    object v = cache.get(key);
    if (v == null) {
        lock.lock();
        try {
            v = cache.get(key);
            if (v == null) {
                v = db.query(key);
                cache.put(key, v);
            }
        } finally {
            lock.unlock();
        }
    }
    return v;
}
  • 重建缓存的等待时间主要花在 db 查询上
  • 锁本身持有时间相对较短
  • 用公平锁反而拖慢整体响应

场景 c:短临界区资源池

// 数据库连接池、线程池内部的任务队列
// 锁保护的就是"取出/放入一个对象"的动作
  • 临界区是 queue.poll() / queue.offer() 一次操作
  • 公平锁的唤醒开销不划算

6.2 选公平锁的场景

特点:锁持有时间长,绝对不能容忍 "先到的一直拿不到"。

提示:公平锁仅保证排队顺序,不能解决任务堆积问题,业务上游仍需要做任务限流,避免大量任务堆积在锁队列。

场景 a:超图 .smwu 出图

private final map<string, reentrantlock> workspacelockmap = new concurrenthashmap<>();
reentrantlock lock = new reentrantlock(true); // 公平锁
// lock.lockinterruptibly(); // 获取锁过程响应线程中断,无超时,会永久阻塞
// domap(...);   // 出图 5~15 秒
// lock.unlock();
  • 每张图出图要 5~15 秒(栅格裁剪 + dem 统计 + 渲染 + io)
  • 必须保证 "先启动的图先出图"
  • 否则后启动的图全部插队到前面,先到的图会等满 60s 超时

⚠️业务风险提醒:lockinterruptibly() 没有超时能力,如果某个任务卡死没有 unlock,所有后续任务会永久阻塞;如果需要保留原有 60s 超时逻辑,应当使用带超时的 trylock(60, timeunit.seconds)

场景 b:文件 / 打印机独占

// 多个任务往同一文件写日志
// 多个任务调用同一台打印机
  • 写一次日志可能要几毫秒到几秒
  • 公平排队能让每个任务有可预测的等待时间

场景 c:交易 / 订单处理

// 同一账户的多个并发扣款
// 必须按用户请求的顺序处理
  • 业务上严格要求时序
  • "a 先下单应该先扣款,结果 b 先扣了" 是不能接受的

场景 d:限流代理

// 本地缓存代理外部限流接口(每秒只能调 10 次)
// 所有调用方都要排队
  • 锁内要做 "等令牌 → 调外部 → 解析"
  • 业务要求 "先到先调"

七、决策树

锁内操作时间?
├── 极短(ns/μs)
│   └── 业务能容忍饿死吗?
│       ├── 能 → 非公平锁
│       └── 不能 → 公平锁(或换无锁方案)
│
└── 较长(ms/s)
    └── 业务能容忍饿死吗?
        ├── 能 → 非公平锁(性能稍好)
        └── 不能 → 公平锁(推荐)

更简洁的版本:

如果你在乎 "先到先服务",就用公平锁。
如果你只在乎 "整体吞吐最大",就用非公平锁。

八、选型对照表

场景推荐理由
内存计数器、缓存重建非公平临界区极短,吞吐优先
出图、报表、pdf 生成公平持有时间长,绝不能饿死
文件独占写公平持有时间中等,要求有序
订单 / 账户处理公平业务要求时序
短 cas 保护非公平临界区 ns 级
限流代理、外部资源串行调用公平持有时间取决于外部,不可预测
数据库连接池内部非公平临界区极短
多线程出同一份 .smwu公平持有时间长,业务强一致

九、实战案例:.smwu 出图为什么必须用公平锁

9.1 业务背景

地震应急出图系统,多个专题图(居民点、地质灾害、震中等)共用同一份 gsdzyjct.smwu 工作空间。

idesktop java sdk 不支持多线程并发访问同一 .smwu,必须串行。

旧代码用 synchronized + trylock(60s):排队是 "看谁先抢到",不是 "看谁先来"。

9.2 时间线复盘

08:53:15.855  earthquake_epicenter 启动
08:53:15.855  earthquake_epicenter 等待锁(先到)
08:53:18.xxx  eq_station 启动
08:53:18.xxx  eq_station 等待锁(后到)
08:53:38.725  eq_station 拿到锁(先释放的先抢)
08:53:38.725  earthquake_epicenter 还在等
08:53:50.xxx  eq_fracture 启动(更晚到)
08:53:50.xxx  eq_fracture 直接抢到锁(插队!)
08:53:50.xxx  earthquake_epicenter 还在等
08:54:00.865  eq_fracture 释放
08:54:15.862  earthquake_epicenter 60s 超时放弃
08:54:24.xxx  eq_traffic 才完成(最后才轮到)

9.3 问题分析

  • earthquake_epicenter 是先到的,应该优先服务
  • 但因为非公平,后到的 eq_fracture、eq_traffic 反而先抢到
  • earthquake_epicenter 一直等不到,60s 超时后被丢弃,这张图就缺了

9.4 公平锁改造

方案一:lockinterruptibly,无超时,会永久阻塞

reentrantlock lock = new reentrantlock(true);  // 公平锁
lock.lockinterruptibly();                       // 获取锁过程响应线程中断,无超时,会永久阻塞
try {
    domapinner(...);                            // 出图
} finally {
    lock.unlock();
}

方案二【业务推荐】:保留原有 60s 超时,公平锁下带超时 trylock,兼顾排队与超时防护

reentrantlock lock = new reentrantlock(true);
boolean locked = lock.trylock(60, timeunit.seconds);
if (!locked) {
    throw new timeoutexception("获取工作空间锁超时60s");
}
try {
    domapinner(...);
} finally {
    lock.unlock();
}

改造后时序:

08:53:15.855  earthquake_epicenter 入队
08:53:18.xxx  eq_station 入队
08:53:50.xxx  eq_fracture 入队
释放锁后按顺序出锁:
  1. earthquake_epicenter(先到先服务)✅
  2. eq_station
  3. eq_fracture
  ...
每张图最终都会轮到,不会饿死。

十、常见误区

误区 1:公平锁一定慢

错。锁持有时间越长,公平锁的性能损失越小。

在 "ms/s 级" 临界区下,公平锁与非公平锁的性能差异 < 5%。

误区 2:默认就用非公平

不一定。jdk 默认非公平是因为 "大部分场景临界区短、吞吐优先"。

但在你的业务场景下,必须根据业务特性来选。

误区 3:公平锁就一定好

错。公平锁也有适用场景,不是银弹。

短临界区用公平锁反而拖慢整体性能。

误区 4:synchronized 是公平锁

错。synchronized 是非公平锁,不能保证 fifo。hotspot 的 monitor 锁队列没有公平开关。

误区 5:公平锁实例所有 api 都遵守公平排队

错。公平锁实例调用无参 trylock() 直接 cas 抢锁,可以插队;

只有 lock() / lockinterruptibly() / trylock(time,unit) 才执行公平判断。

十一、总结

短临界区 + 不在乎顺序 = 非公平;长临界区 + 必须有序 = 公平。

选用原则就两句话:

  • 在乎 "先到先服务" → 公平锁
  • 在乎 "整体吞吐最大" → 非公平锁

你的 .smwu 出图属于 "长临界区 + 必须有序",必须用公平锁

非公平锁在这种场景下不是性能优化,而是bug

额外业务提醒:

公平锁不等于不会阻塞,长临界区业务务必加上锁等待超时,防止任务卡死导致全量阻塞;

警惕无参 trylock() 破坏公平语义;

公平锁只保证应用层排队顺序,上游需要限流,避免任务大量堆积锁队列。

附录:核心代码片段

a. 公平锁的标准用法

public class fairlockdemo {
    private final reentrantlock lock = new reentrantlock(true);
    public void dowork() {
        lock.lock();
        try {
            // 临界区
        } finally {
            lock.unlock();
        }
    }
}

b. .smwu 出图的公平锁改造(推荐带超时版本)

private void domap(string eqid, string eqtaskid, string filepath, string maptype,
                   string eqtime, string eqaddr, string eqlon, string eqlat,
                   string eqdepth, string eqmag, string maptitle,
                   map<string, object> mapstyle, workspace workspace, string workspacepath) throws timeoutexception, interruptedexception {
    reentrantlock lock = workspacelockmap.get(workspacepath);
    if (lock == null) {
        synchronized (workspacelockmap) {
            lock = workspacelockmap.get(workspacepath);
            if (lock == null) {
                lock = new reentrantlock(true); // 公平锁
                workspacelockmap.put(workspacepath, lock);
            }
        }
    }
    boolean locked = false;
    try {
        log.info("【{}】等待工作空间锁 | path={}", maptype, workspacepath);
        // 公平锁,最多等待60秒,响应中断
        locked = lock.trylock(60, timeunit.seconds);
        if (!locked) {
            throw new timeoutexception("获取工作空间锁超时60s, path:" + workspacepath);
        }
        log.info("【{}】拿到工作空间锁 | path={}", maptype, workspacepath);
        domapinner(eqid, eqtaskid, filepath, maptype, eqtime, eqaddr,
                eqlon, eqlat, eqdepth, eqmag, maptitle, mapstyle, workspace, workspacepath);
    } finally {
        if (locked) {
            lock.unlock();
        }
    }
}

c. 锁关键字纳入可恢复异常白名单

private boolean isrecoverableexception(throwable t) {
    if (t == null) return false;
    string msg = t.getmessage();
    if (msg == null) return false;
    string low = msg.tolowercase();
    return low.contains("workspace")
        || low.contains("datasource")
        || low.contains("timeout");
        // 提示:线上环境异常栈多为英文,不建议匹配中文关键字"锁、工作空间",会出现匹配失效
}

到此这篇关于java 并发编程:reentrantlock 公平锁与非公平锁深度解析的文章就介绍到这了,更多相关java reentrantlock 公平锁与非公平锁内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com