有情怀,有干货,微信搜索【三太子敖丙】关注这个有一点点东西的程序员。java
本文 GitHub github.com/JavaFamily 已收录,有一线大厂面试完整考点、资料以及个人系列文章。node
在Java多线程编程中,重入锁(ReentrantLock) 和信号量(Semaphore)是两个极其重要的并发控制工具。相信大部分读者都应该比较熟悉它们的使用(若是不清楚的小伙伴,赶快拿出书本翻阅一下)。git
可是不知道你们是否是有了解太重入锁和信号量的实现细节? 我就带你们看一看它们的具体实现。程序员
首先,先上一张重要的类图,来讲明一下三者之间的关系:github
能够看到, 重入锁和信号量都在本身内部,实现了一个AbstractQueuedSynchronizer的子类,子类的名字都是Sync。而这个Sync类,也正是重入锁和信号量的核心实现。子类Sync中的代码也比较少,其核心算法都由AbstractQueuedSynchronizer提供。所以,能够说,只要你们了解了AbstractQueuedSynchronizer,就清楚得知道重入锁和信号量的实现原理了。面试
在正是进入AbstractQueuedSynchronizer以前,还有一些基础知识须要你们了解,这样才能更好的理解AbstractQueuedSynchronizer的实现。算法
为了控制多个线程访问共享资源 ,咱们须要为每一个访问共享区间的线程派发一个许可。拿到一个许可的线程才能进入共享区间活动。当线程完成工做后,离开共享区间时,必需要归还许可,以确保后续的线程能够正常取得许可。若是许可用完了,那么线程进入共享区间时,就必须等待,这就是控制多线程并行的基本思想。编程
打个比方,一大群孩子去游乐场玩摩天轮,摩天轮上只能坐20个孩子。可是却来了100个小孩。那么许能够的个数就是20。也就说一次只有20个小孩能够上摩天轮玩,其余的孩子必须排队等待。只有等摩天轮上的孩子离开控制一个位置时,才能有其余小孩上去玩。微信
所以,使用许可控制线程行为和排队玩摩天轮差很少就是一个意思了。markdown
第二个重要的概念就是排他锁(exclusive)和共享锁(shared)。顾名思义,在排他模式上,只有一个线程能够访问共享变量,而共享模式则容许多个线程同时访问。简单地说,重入锁是排他的;信号量是共享的。
用摩天轮的话来讲,排他锁就是虽然我这里有20个位置,可是小朋友也只能一个一个上哦,多出来的位置怎么办呢,能够空着,也可让摩天轮上惟一的小孩换着作,他想坐哪儿就坐哪儿,1分钟换个位置,都没有关系。而共享锁,就是玩耍摩天轮正常的打开方式了。
LockSupport能够理解为一个工具类。它的做用很简单,就是挂起和继续执行线程。它的经常使用的API以下:
由于单词park的意思就是停车,所以这里park()函数就表示让线程暂停。反之,unpark()则表示让线程继续执行。
须要注意的是,LockSupport自己也是基于许可的实现,如何理解这句话呢,请看下面的代码:
LockSupport.unpark(Thread.currentThread());
LockSupport.park();
复制代码
你们能够猜一下,park()以后,当前线程是中止,仍是 能够继续执行呢?
答案是:能够继续执行。那是由于在park()以前,先执行了unpark(),进而释放了一个许可,也就是说当前线程有一个可用的许可。而park()在有可用许可的状况下,是不会阻塞线程的。
综上所述,park()和unpark()的执行效果和它调用的前后顺序没有关系。这一点至关重要,由于在一个多线程的环境中,咱们每每很难保证函数调用的前后顺序(都在不一样的线程中并发执行),所以,这种基于许可的作法可以最大限度保证程序不出错。
与park()和unpark()相比, 一个典型的反面教材就是Thread.resume()和Thread.suspend()。
看下面的代码:
Thread.currentThread().resume();
Thread.currentThread().suspend();
复制代码
首先让线程继续执行,接着在挂起线程。这个写法和上面的park()的示例很是接近,可是运行结果倒是大相径庭的。在这里,当前线程就是卡死。
所以,使用park()和unpark()才是咱们的首选。而在AbstractQueuedSynchronizer中,也正是使用了LockSupport的park()和unpark()操做来控制线程的运行状态的。
好了,基础的部分就介绍到这里。下面,让咱们切入正题:首先来看一下AbstractQueuedSynchronizer的内部数据结构。
在AbstractQueuedSynchronizer内部,有一个队列,咱们把它叫作同步等待队列。它的做用是保存等待在这个锁上的线程(因为lock()操做引发的等待)。此外,为了维护等待在条件变量上的等待线程,AbstractQueuedSynchronizer又须要再维护一个条件变量等待队列,也就是那些由Condition.await()引发阻塞的线程。
因为一个重入锁能够生成多个条件变量对象,所以,一个重入锁就可能有多个条件变量等待队列。实际上,每一个条件变量对象内部都维护了一个等待列表。其逻辑结构以下所示:
下面的类图展现了代码层面的具体实现:
能够看到,不管是同步等待队列,仍是条件变量等待队列,都使用同一个Node类做为链表的节点。对于同步等待队列,Node中包括链表的上一个元素prev,下一个元素next和线程对象thread。对于条件变量等待队列,还使用nextWaiter表示下一个等待在条件变量队列中的节点。
Node节点另一个重要的成员是waitStatus,它表示节点等待在队列中的状态:
其中CANCELLED=1,SIGNAL=-1,CONDITION=-2,PROPAGATE=-3 。在具体的实现中,就能够简单的经过waitStatus释放小于等于0,来判断是不是CANCELLED状态。
了解了AbstractQueuedSynchronizer的基本实现思路和数据结构,接下来一块儿看一下它的实现细节吧。首先,来看一下排他锁的实现。重入锁是一种 典型的排他锁。
下面是排他锁得到请求许可的代码:
public final void acquire(int arg) {
//尝试得到许可, arg为许可的个数。对于重入锁来讲,每次请求1个。
if (!tryAcquire(arg) &&
// 若是tryAcquire 失败,则先使用addWaiter()将当前线程加入同步等待队列
// 而后继续尝试得到锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
复制代码
进入一步看一下tryAcquire()函数。该函数的做用是尝试得到一个许可。对于AbstractQueuedSynchronizer来讲,这是一个未实现的抽象函数。
具体实如今子类中。在重入锁,读写锁,信号量等实现中, 都有各自的实现。
若是tryAcquire()成功,则acquire()直接返回成功。若是失败,就用addWaiter()将当前线程加入同步等待队列。
接着, 对已经在队列中的线程请求锁,使用acquireQueued()函数,从函数名字上能够看到,其参数node,必须是一个已经在队列中等待的节点。它的功能就是为已经在队列中的Node请求一个许可。
这个函数你们要好好看看,由于不管是普通的lock()方法,仍是条件变量的await()都会使用这个方法。
若是调用Condition.await(),那么线程也会进入等待,下面来看实现:
signal()通知的时候,是在条件等待队列中,按照FIFO进行,首先从第一个节点下手:
释放排他锁很简单
public final boolean release(int arg) {
//tryRelease()是一个抽象方法,在子类中有具体实现和tryAcquire()同样
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 从队列中唤醒一个等待中的线程(遇到CANCEL的直接跳过)
unparkSuccessor(h);
return true;
}
return false;
}
复制代码
与排他锁相比,共享锁的实现略微复杂一点。这也很好理解。由于排他锁的场景很简单,单进单出,而共享锁就不同了。多是N进M出,处理起来要麻烦一些。可是,他们的核心思想仍是一致的。共享锁的几个典型应用有:信号量,读写锁中的写锁。
为了实现共享锁,在AbstractQueuedSynchronizer中,专门有一套针对共享锁的方法。
得到共享锁使用acquireShared()方法:
释放共享锁的代码以下:
public final boolean releaseShared(int arg) {
//tryReleaseShared()尝试释放许可,这是一个抽象方法,须要在子类中实现
if (tryReleaseShared(arg)) {
//上述代码中已经出现这个函数了,就是唤醒线程,设置传播状态
doReleaseShared();
return true;
}
return false;
}
复制代码
AbstractQueuedSynchronizer 是一个比较复杂的实现,要彻底理解其中的细节还须要慢慢琢磨。
这篇文章也只能起到一个抛砖引玉的做用,将AbstractQueuedSynchronizer的设计思想,核心数据结构已经核心实现代码展现给你们。但愿对你们理解AbstractQueuedSynchronizer的实现,以及理解重入锁,信号量,读写锁有必定帮助。
多线程系列还在在路上会继续安排,小傻瓜若是对这个类有深一步的理解,能够在评论区来一波:变得更强
我是敖丙,你知道的越多,你不知道的越多,感谢各位人才的:点赞、收藏和评论,咱们下期见!
文章持续更新,能够微信搜一搜「 三太子敖丙 」第一时间阅读,回复【资料】有我准备的一线大厂面试资料和简历模板,本文 GitHub github.com/JavaFamily 已经收录,有大厂面试完整考点,欢迎Star。