new ReentrantLock() 默认是非公平的,传 true 才是公平锁——但"公平"具体公平在哪、要付出多大代价,真实起线程抢锁量出来,比记一句"公平锁更慢"实在得多。
上一篇:ConcurrentHashMap:多线程 put 为什么不会锁住整张表
4 个线程各抢锁 2 万次,统计"同一个线程连续拿到锁"这件事最多发生了多少次。
如果锁是绝对公平的,理论上不会有任何一个线程连续两次拿到锁而不轮到别人——每次释放都该轮到排在队首的下一个线程。真实统计一下同一线程连续拿锁的最长streak:
核心逻辑 for (int i = 0; i < ITER; i++) { lock.lock(); try { // 记录:这次拿到锁的线程,是不是跟上一次同一个线程 // 是就把连续计数 +1,不是就重置成 1,全程跟踪最大值 } finally { lock.unlock(); } }
=== 非公平锁(默认) === 耗时: 7ms 同一个线程连续拿到锁的最长连续次数: 2346 === 公平锁 === 耗时: 411ms 同一个线程连续拿到锁的最长连续次数: 134非公平锁:同一个线程曾经连续 2346 次抢到锁,其它 3 个线程这段时间里完全没插上手 公平锁:最长连续也只有 134 次,插队严重程度差了一个数量级
再独立跑一遍,确认不是偶然:
非公平锁: 耗时 7ms,最长连续 2153 次 公平锁 : 耗时 244ms,最长连续 15 次
两次运行,非公平锁的耗时稳定在 7ms 左右,公平锁的耗时在 244~411ms 之间波动——具体倍数会变,但"非公平锁明显更快、连续插队次数明显更高"这个趋势两次完全一致。有意思的是,两种锁最终四个线程拿到锁的总次数都精确是 20000 次一个不少——公平性影响的是"谁先拿到",不影响"最终拿到的次数是不是准确",这本身也是锁本该保证的基本正确性。
非公平锁在锁被释放的瞬间,谁都可以直接尝试抢——包括刚刚释放锁、还没来得及被系统真正挂起的那个线程自己。如果它反应够快,经常能在其它排队线程被操作系统真正唤醒调度之前,直接原地再次抢到锁,完全跳过"把一个线程从阻塞状态唤醒、参与调度、切换上下文"这一整套昂贵的操作。这也是为什么它的耗时能做到公平锁的几十分之一——本质是省掉了大量线程唤醒和上下文切换的开销。
公平锁则严格规定:只要队列里还有比你等得更久的线程,你就必须排到它后面,哪怕你人在锁旁边、它还在系统调度队列里没醒。这意味着几乎每一次释放锁,都要真的去唤醒下一个排队线程、等它被调度器选中、完成上下文切换——这一整套流程的代价,远比"运气好直接抢到手"要高得多。
每一行都是真实运行这段代码得到的结果。