《蹲坑也能进大厂》多线程系列 - 非公平锁和公平锁详解

这是我参与更文挑战的第 17 天,活动详情查看:更文挑战

作者:花Gie

微信公众号:Java开发零到壹

前言

多线程系列我们前面已经更新过很多章节,强烈建议小伙伴按照顺序学习:

《蹲坑也能进大厂》多线程系列文章目录

前一章介绍了悲观锁、乐观锁以及CAS等概念,今天就继续介绍另外的两种常见的锁—-非公平锁和公平锁

正文

锁的总览

锁可以从不同的角度进行分类,这些分类并不是互斥的,比如一个人既可以是医生,又可以是父亲,也可以是一样女人。分类总览如图:

Java锁分类

公平锁与非公平锁

  • 概念

公平锁:多个线程申请获取锁,会按照先后顺序进入队列,排在队首的线程会率先获取锁。

非公平锁:多个线程申请锁时,首先会尝试获取锁,如果获取不到,再进行排队,如果能够直接获取到锁,那么直接获取。

这里举个生活中的常见例子,这样大家就很容易理解。

小伙伴们应该都去过火车站排队买票吧,现在估计都会在网上直接购票,那排队就是一个很公平的,先到先得,这就是公平锁的案例。

image.png

如果这时有个乘客1买完票了,乘客2正在掏身份证,此时有个乘客4上来插队,问了售票员一句:去铁岭的票还有吗?这个时候就可以看做是非公平锁了,因为乘客4没有按照先来后到而是直接见缝插针,抢先一步,这就是非公平锁的一种案例。

image.png

说完了生活实例,我们接下来看一下这两种锁的原理实现。

这就涉及到我们前面文章经常提到的ReentrantLock,它可实现公平锁和非公平锁两种模式,而ReentrantLock的实现是基于其内部类Sync。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;

/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;

Sync又有两个子类,FairSync(公平锁)和NonFairSync(非公平锁)。

image.png

实现原理

公平锁与非公平锁的原理差距不大,主要是在获取锁时存在差异,公平锁在获取锁之前会先判断等待队列是否为空或者自己是否位于队列头部,该条件通过才能继续获取锁。两种获取锁的方式对应源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java复制代码//公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//hasQueuedPredecessors:用于检查是否有等待队列的。
//有线程来抢占锁的时候,都会检查一遍有没有等待队列
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

//非公平锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//无需判断是否有等待队列
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

从上面两个方法源码可以看到,非公平锁在尝试获取锁时,不会调用hasQueuedPredecessors(判断当前的线程是否位于同步队列的首位,是就返回true,否则返回false)。

1
2
3
4
5
6
7
8
9
10
java复制代码public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

对于非公平锁,只要线程进入了等待队列,队列里面依然是先进先出的原则,和公平锁的顺序是一样的。因为公平锁与非公平锁在释放锁部分代码是共用AQS的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);

Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒队列头的线程
LockSupport.unpark(s.thread);
}

代码实现

这里写一个多线程打印日志的案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
java复制代码public class FairLockDemo {

public static void main(String[] args) {
PrintQueue printQueue = new PrintQueue();
Thread thread[] = new Thread[10];
//1.创建5个线程
for (int i = 0; i < 5; i++) {
thread[i] = new Thread(new Job(printQueue));
}
//2.启动这5个线程
for (int i = 0; i < 5; i++) {
thread[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

class Job implements Runnable {
PrintQueue printQueue;

public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}

@Override
public void run() {
//3.线程启动完成后,未获取到锁的线程阻塞到printJob前,等待获取锁
System.out.println(Thread.currentThread().getName() + "开始打印");
printQueue.printJob();
System.out.println(Thread.currentThread().getName() + "打印完毕");
}
}

//打印类
class PrintQueue {
private Lock queueLock = new ReentrantLock(false);

public void printJob() {
queueLock.lock();
try {
//4.模拟打印,调用sleep休眠随机秒数
int duration = new Random().nextInt(5) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
//5.当前线程释放锁后,立马再次尝试获取锁
queueLock.lock();
try {
//6.模拟打印,调用sleep休眠随机秒数
int duration = new Random().nextInt(5) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration+"秒");
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
}

根据上面6个步骤,我们分别尝试ReentrantLock实现公平锁和非公平锁模式,会出现什么结果呢。

  • 使用公平锁模式,即ReentrantLock(true),每个线程按照先后顺序,插入到队列中。如下图按照Thread-0、1、2、3、4打印一遍之后,再次按照该顺序打印第二遍,不会因为步骤5出现线程插队现象:

image.png

  • 使用非公平锁模式,即ReentrantLock(false),当一个线程执行完步骤4后,会立即尝试获取锁(步骤5),因此该线程会抢先队列中的线程,从而获取到锁。但是整体又是按照Thread-0、1、2、3、4进行执行,这也符合我们上面对非公平锁的分析:

image.png

总结

上面是公平锁和非公平锁的介绍,文中涉及到AQS、ReentrantLock,可能小伙伴们不是很理解,不过不用担心,这里我们先熟悉概念,这几张介绍锁的概念也是在为后面AQS进行铺垫。

点关注,防走丢

以上就是本期全部内容,如有纰漏之处,请留言指教,非常感谢。我是花GieGie ,有问题大家随时留言讨论 ,我们下期见🦮。

文章持续更新,可以微信搜一搜 Java开发零到壹 第一时间阅读,并且可以获取面试资料学习视频等,有兴趣的小伙伴欢迎关注,一起学习,一起哈🐮🥃。

原创不易,你怎忍心白嫖,如果你觉得这篇文章对你有点用的话,感谢老铁为本文点个赞、评论或转发一下,因为这将是我输出更多优质文章的动力,感谢!

参考链接:

blog.csdn.net/qq_29479041…

www.imooc.com/article/302…

segmentfault.com/a/119000001…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%