赞
踩
有情怀,有干货,微信搜索【三太子敖丙】关注这个有一点点东西的程序员。
本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试完整考点、资料以及我的系列文章。
在Java多线程编程中,重入锁(ReentrantLock) 和信号量(Semaphore)是两个极其重要的并发控制工具。相信大部分读者都应该比较熟悉它们的使用(如果不清楚的小伙伴,赶快拿出书本翻阅一下)。
但是不知道大家是不是有了解过重入锁和信号量的实现细节? 我就带大家看一看它们的具体实现。
首先,先上一张重要的类图,来说明一下三者之间的关系:
可以看到, 重入锁和信号量都在自己内部,实现了一个AbstractQueuedSynchronizer的子类,子类的名字都是Sync。而这个Sync类,也正是重入锁和信号量的核心实现。子类Sync中的代码也比较少,其核心算法都由AbstractQueuedSynchronizer提供。因此,可以说,只要大家了解了AbstractQueuedSynchronizer,就清楚得知道重入锁和信号量的实现原理了。
在正是进入AbstractQueuedSynchronizer之前,还有一些基础知识需要大家了解,这样才能更好的理解AbstractQueuedSynchronizer的实现。
为了控制多个线程访问共享资源 ,我们需要为每个访问共享区间的线程派发一个许可。拿到一个许可的线程才能进入共享区间活动。当线程完成工作后,离开共享区间时,必须要归还许可,以确保后续的线程可以正常取得许可。如果许可用完了,那么线程进入共享区间时,就必须等待,这就是控制多线程并行的基本思想。
打个比方,一大群孩子去游乐场玩摩天轮,摩天轮上只能坐20个孩子。但是却来了100个小孩。那么许可以的个数就是20。也就说一次只有20个小孩可以上摩天轮玩,其他的孩子必须排队等待。只有等摩天轮上的孩子离开控制一个位置时,才能有其他小孩上去玩。
因此,使用许可控制线程行为和排队玩摩天轮差不多就是一个意思了。
第二个重要的概念就是排他锁(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,它表示节点等待在队列中的状态:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。