和朋友聊天他提到:ReentrantLock 的构造函数可以传递一个 bool 数据,true 时构造的是“公平锁”、false 时构造的是“非公平锁”。
我的印象中锁是不区分类型的,所以认为这应该是 Java 发明的概念,于是就恶补了一下。
锁的底层实现
无论什么语言在操作系统层面锁的操作都会变成系统调用(System Call),以 Linux 为例,就是 futex 函数,可以把它理解为两个函数: futex_wait(s),对变量 s 加锁;futex_wake(s)释放 s 上的锁,唤醒其他线程。
如果你熟悉操作系统原理其实就是 P/V 操作。
Java 公平锁和非公平锁
公平锁的 lock 操作是调用futex_wait,unlock 操作是调用futex_wake。比如下面的代码
非公平锁的 lock/unlock 操作会先做一次 CAS 操作然后再调用 futex_wait、futex_wake。比如下面的代码
在上锁之前增加了一个 CAS 原子操作,它接受三个变量可以把它理解为下面的逻辑:
第一个参数的值和第二个参数不相等则返回 0 表示操作失败否则更新为新的值。这个函数不是由代码实现的而是 CPU 提供的一个指令,比如 Intel 的叫 cmpxchg;高级语言进行了封装,比如 Java 的 Atomic 变量。
为什么
明白了原理再来提问为什么,在上锁之前先通过 CAS 修改一个变量表示“我要上锁”了看似很冗余的操作,其实它是一次自旋,如果资源很快被使用完可以提高系统的吞吐率。考虑下面的场景
上锁之前的时间是 t1,上锁之后是 t2(使用资源),释放锁是 t3。
现在有两个线程,处于 t1 状态,其中 A 线程先抢到资源处于 t2 ;B 线程也会尝试 lock,与此同时 t2 释放了,而 lock 动作也执行成功了 B 被挂起;系统继续执行 A 释放成功唤醒 B 继续执行。
上述过程中 B 只要再多等待“一丢丢”就不用被挂起,直接获得资源继续执行。非公平锁的 CAS 操作就是为了增加一丢丢时间。
采用非公平锁,如果系统中有 3 个线程执行,A 抢到资源,C 没有抢到处于挂起状态,此时 B 尝试 CAS 操作,而 A 刚好释放掉资源还没有来得及唤醒 C,那么 B 会先抢到资源,在 C 之前执行。这就是“非公平”的来历,虽然 C 老老实实的等待了很长时间,但是 B 的“时机”把握的好,迅速“插队”完成资源抢占。
总结
上锁的过程本身也是有时间开销的,如果操作资源的时间比上锁的时间还短建议使用非公平锁可以提高系统的吞吐率;否则就老老实实的用公平锁。
【本文是51CTO专栏作者“邢森”的原创文章,转载请联系作者本人获取授权】