公平锁/非公平锁
什么是公平锁和非公平锁?
公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。 非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。
举个例子,公平锁就像开车经过收费站一样,所有的车都会排队等待通过,先来的车先通过,如下图所示:
通过收费站的顺序也是先来先到,分别是张三、李四、王五,这种情况就是公平锁。 而非公平锁相当于,来了一个强行加塞的老司机,它不会准守排队规则,来了之后就会试图强行加塞,如果加塞成功就顺利通过,当然也有可能加塞失败,如果失败就乖乖去后面排队,这种情况就是非公平锁。
应用场景
在 Java 语言中,锁 synchronized 和 ReentrantLock 默认都是非公平锁,当然我们在创建 ReentrantLock 时,可以手动指定其为公平锁,但 synchronized 只能为非公平锁。 ReentrantLock 默认为非公平锁可以在它的源码实现中得到验证,如下源码所示: 当使用 new ReentrantLock(true) 时,可以创建公平锁,如下源码所示:
公平和非公平锁代码演示
public static void main(String[] args) {
//测试公平锁
ReentrantLock fairLock = new ReentrantLock(true);
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"开始");
try {
fairLock.lock();
System.out.println(Thread.currentThread().getName()+"获得锁");
try {
//模拟业务逻辑
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
fairLock.unlock();
}
System.out.println(Thread.currentThread().getName()+"结束");
}).start();
}
}
结果如下,可以看到Thread-0比Thread-5先开始,但是Thread-0却比Thread-5后获得到锁,说明是非公平的,因为Thread-0先排队,被Thread-5插队了
Thread-1开始
Thread-1获得锁
Thread-4开始
Thread-0开始
Thread-5开始
Thread-2开始
Thread-7开始
Thread-6开始
Thread-3开始
Thread-1结束
Thread-4获得锁
Thread-9开始
Thread-11开始
Thread-5获得锁
Thread-4结束
Thread-12开始
Thread-8开始
Thread-13开始
Thread-10开始
Thread-14开始
Thread-15开始
Thread-5结束
Thread-0获得锁
Thread-16开始
Thread-18开始
Thread-19开始
Thread-17开始
Thread-0结束
Thread-2获得锁
Thread-2结束
Thread-7获得锁
Thread-7结束
Thread-6获得锁
Thread-6结束
Thread-3获得锁
Thread-3结束
Thread-9获得锁
Thread-9结束
Thread-11获得锁
Thread-11结束
Thread-12获得锁
Thread-12结束
Thread-8获得锁
Thread-8结束
Thread-13获得锁
Thread-13结束
Thread-10获得锁
Thread-10结束
Thread-14获得锁
Thread-14结束
Thread-15获得锁
Thread-15结束
Thread-16获得锁
Thread-16结束
Thread-18获得锁
Thread-18结束
Thread-19获得锁
Thread-19结束
Thread-17获得锁
Thread-17结束
公平锁的测试如下,排队顺序和获得锁顺序是一致的。
public static void main(String[] args) {
ReentrantLock fairLock = new ReentrantLock(true);
//测试公平锁
for (int i = 0; i < 1000; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"开始");
try {
fairLock.lock();
System.out.println(Thread.currentThread().getName()+"获得锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
fairLock.unlock();
}
System.out.println(Thread.currentThread().getName()+"结束");
}).start();
}
}
执行流程分析
公平锁执行流程 获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。
非公平锁执行流程 当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。 公平锁和非公平锁的性能测试结果如下,以下测试数据来自于《Java并发编程实战》: 上述结果可以看出,使用非公平锁的吞吐率(单位时间内成功获取锁的平均速率)要比公平锁高很多。
优缺点分析
公平锁的优点是按序平均分配锁资源,不会出现线程饿死的情况,它的缺点是按序唤醒线程的开销大,执行性能不高。 非公平锁的优点是执行效率高,谁先获取到锁,锁就属于谁,不会“按资排辈”以及顺序唤醒,但缺点是资源分配随机性强,可能会出现线程饿死的情况
在 Java 语言中,锁的默认实现都是非公平锁,原因是非公平锁的效率更高,使用 ReentrantLock 可以手动指定其为公平锁。非公平锁注重的是性能,而公平锁注重的是锁资源的平均分配,所以我们要选择合适的场景来应用二者。