线程池主要有两种创建方式。
一种是选择使用Executors线程池工具类,可以理解为线程池工厂类,通过该类设置好的一些静态方法,创建指定类型的线程池(本质上就是通过new创建线程池,但是该类为我们设置好了各类型线程池的默认参数)。
第二种是选择自己创建线程池,通过new的方法,设置好创建线程池的七个参数,自己手动创建。但是网上包括阿里都是推荐我们使用Executors的默认创建方式。
点开Executors的创建线程池方法,我们发现它选择使用的workQueue(参数类型是BlockingQueue)任务队列,除了SynchronousQueue(比较特殊,这里不讲)就是LinkedBlockingQueue,并且主要使用的就是LinkedBlockingQueue,那么为什么不使ArrayBlockingQueue呢?
因为我们主要讲AQS这里我先写原因:
因为ArrayBlockingQueue是基于数组实现的,需要在创建时给定长度,所以在不会对阻塞队列空间进行判断的线程池中使用它是需要仔细考虑这一细节的,如果设置的ArrayBlockingQueue长度过长会造成空间的多余浪费,设置的长度过短,可能会导致本来不想拒绝的任务被执行拒绝策略。而LinkedArrayQueue则不会,它默认长度是Integer.MAX_VALUE,这个长度不是代表它初始化的时候会创建这么多个节点,而是说最多可以添加这么多个节点,每次执行put,它都会判断当前队列长度是否大于Integer.MAX_VALUE,如果不大于则添加新节点,当然也可以给LinkedBlockingQueue默认长度来限制队列里的任务数量,但作为默认创建的方式,显然LinkedBlockingQueue更为安全(在程序员没有考虑任务队列长度的情况下)。但是ArrayBlockingQueue的效率是要高于LinkedArrayQueue的,因为LinkedBlockingQueue是基于链表实现的,无论是出队还是入队操作,都需要操作Node节点,会稍微比ArrayBlockingQueue更耗费内存一丢丢。在程序员可以确定队列长度的情况下,使用ArrayBlockingQueue可能更为合适。
ArrayBlcokingQueue原理
参考
https://www.jianshu.com/p/584631fcd9bf
LinkedBlockingQueue原理
前面也讲过了LinkedBlockingQueue是基于链表实现的,那么接下来是它的具体实现细节。
LinkedBlockingQueue主要实现了BlockingQueue接口(该接口定义了阻塞队列的一些必要方法如take,put)和继承了AbstractQueue抽象类(该类定义并实现了部分LinkQueue的必要方法,可以将它理解为以链表为原理实现的一系列队列的抽象父类,包括ConcurrentLinkedQueue等非阻塞链表队列也继承了它,这个类的最顶层父类就是Collection,这里我们不细讲)
这里定义的所有变量都很关键从上往下开始讲
1.静态内部类Node,链表节点的实现类,只包含一个item用于存值,与一个next指向下一个节点,说明这是一个单向链表。
2.final int capacity,这个capacity用于限制链表的最大长度,在用构造方法new LinkedBlockingQueue的时候可以初始化该值,如果没有通过new设置,capacity的默认值将会是Interger.MAX_VALUE,上面也说过了,因为LinkedBlockingQueue是基于链表而非数组,所以capacity并不是说在创建链表的时候就初始化这么多个节点,而是通过put方法添加节点的时候会判断是否当前节点数已达到capacity,如果没达到则创建成功,所以如果没有特殊要求,使用默认值就好,不必担心因为capacity过大而占用大量内存。
3.AtomicInteger count,这个类在我的ArrayBlockingQueue的文章中有写到,本质上底层就是通过unsafe的cas操作在该类中维护了一个volite int类型的变量value,但是因为该类拥有许多unsafe的cas操作方法,所以在修改该变量内的int时可以很方便的使用这些cas方法以保证该类对value操作的原子性,并由于维护的value也用volite修饰,同时也保证了可见性。 count 用该类封装来存放当前的当前队列中的节点个数,保证了无论从put还是take各种需要读取或操作count操作的方法中对count读取的正确性与对count操作的原子性。
4.Node head 与 Node last,重头戏来了,如果看过ReentranLock源码的人应该已经发现了,这与AQS的数据结构特别相似,由head指向队列头节点,last指向队列尾节点,我们直到AQS维护的数据结构也是由类似的head指向头节点,tail指向尾节点,并且队列中会维护一个不存放数据的空节点。LinkedBlockingQueue也会一样维护一个空节点吗?这里我可以说是的,等下我们看构造方法的时候就可以证明。
5.Condition notEmpty和Condition notFull,两个阻塞队列,分别用于阻塞在线程为空时调用出队方法的线程与线程已满时调用入队操作的线程。(具体可以看ArrayBlockingQueue原理)
6.ReentrantLock takeLock和putLock,我们知道ArrayBlockingQueue中只有一个ReentrantLock lock用于实现队列的同步,在ArrayBlockingQueue中无论是入队方法还是出队方法都要使用该锁,而LinkedBlockingQueue选择使用两个锁,分别用于对入队和出队操作进行加锁。我们可以将其理解为分段锁,这样它的读写效率是要高于ArrayBlockingQueue的。
但同时又产生了一个问题:用两把锁分别进行读写,如何保证链表结构的线程安全?
这就是AQS数据结构的神奇之处了。
我们先看它的构造方法,无参构造方法里首先证明了我之前说的capacity默认值为Integer.MAX_VALUE的问题,接着看有参构造方法,它确实初始化了一个值为null的空Node,并讲last和head都指向它。LinkedBlockingQueue确实在内部维护了一个迷你版的AQS。
如果不使用AQS的哨兵节点可能存在哪些问题?
我们这里把这个AQS中空值的头节点称为哨兵节点。
以下为无哨兵节点的情况:
我们要知道ArrayBlockingQueue和LinkedBlockingQueue都是阻塞队列,当队列为空尝试出队操作的线程和队列已满尝试入队操作的线程是都会被阻塞的。且由于他们都是队列的数据结构,队头入队,队尾出队,所以他们的竞争情况极小,如果队列中有很多节点(未满的情况)入队和出队操作都是对分别对头尾的节点进行操作,中间隔着好几个节点,它们互不干扰。当队列无节点的时候,由于阻塞队列的特性,执行出队操作的线程被阻塞,会保证执行入队操作先执行,也不存在竞争,当队列满节点的时候同理。
我们发现阻塞队列想要存在竞争关系,只有一种情况,当队列里只有一个节点的时候(这时出队操作要删除的节点是这个节点入队操作也是需要修改这个节点的next指向新的节点),假设无哨兵节点,
那么可能存在这样一种情况:
出队的线程在执行dequeue方法之前(该方法为执行出队的具体操作,即修改head指向下一个节,将头节点的引用指向自己,实现头节点的GC回收),判断队列里存在一个头节点,但是在具体执行dequeue方法的时候由于线程的并发性,执行入队节点的线程并不知道头节点会被删除,还是会调用enqueue方法(该方法为执行入队的具体操作,即修改上一个节点的next指向入队节点并修改last节点指向入队节点,实现入队),将头节点的next指向入队节点(这里有点绕这里的情况是head和last都指向队列中唯一一个节点,虽然是通过last.next=new Node来添加新节点,但是不可能实时判断last指向是否为空,可能上一秒判断last指向头节点不为空,开始执行入队操作的时候last却已经为空,那么使用last.next就会抛空指针异常),这是很危险的,无法保证判断last不为空,使用last的时候也不为空(last指向的头节点随时可能出队,last和head就会指向null)。
为什么AQS能实现队列线程安全?
点开enqueue方法,和dequeue方法,我们逐行分析,看看AQS是如何实现队列线程安全的。
很简单的实现,就是把last指向的尾节指向新入队的节点,并把last也指向新入队的节点,实现节点的添加。
1.先用h拿到head指向的头节点(AQS中的哨兵节点)。
2.接着用first拿到哨兵节点后的第一个节点(第一个存值的节点)。
3.让哨兵节点的next指向哨兵节点自己(实现GC回收)。
4.把head指向头first(first为此时队列中实际的头节点,并失去head对前哨兵节点的引用,此时前哨兵节点已可以被回收)
5-8.获取头节点的值并返回该值,且返回前把头节点的值置为null。(此时头节点成为新的哨兵节点)
我们可以看到enqueue和dequeue分别操作的是head和头节点及last和尾节点,如果使用AQS的哨兵模式,保证队列里至少有两个节点(一个节点的时候就是队列中除了哨兵节点无其他节点的情况,此时会认为队列中无元素,那么尝试出队操作的线程会被阻塞,不存在竞争),当队列里有两个节点的时候头尾节点分别执行入队出队操作,它们并不会对头节点指向尾节点的next进行操作(不会破坏两个节点之间的关系而是操作各自的节点及各自的head/last),也就是本质上是互不干扰的,也就自然而然不存在竞争问题。
AQS为什么必须有哨兵节点
1.如果没有哨兵节点,那么每次执行入队操作,都需要判断head是否为空,如果为空则head=new Node如果不为空则head.next=new Node,而有哨兵节点则可以大胆的head.next=new Node.
2.如果没有哨兵节点,可能存在之前所说的安全性问题,当只有一个节点的时候执行入队方法,无法保证last和head不为空。哪怕执行enqueue入队之前last和head还指向一个节点,可能由于并发性在具体调用enqueue方法操作last的时候head和last共同指向的头节点已经完成出队,此时last和head都为null,所以enqueue方法中的last.next=new node会抛空指针异常,且由于线程并发性的问题,last始终可能随时为空的问题不使用哨兵节点是无法解决的。