【三】关于java.util.concurrent包下的并发类(concurrent)

终于到了并发类中的重头戏concurrent包。该包下的ConcurrentMap这一类LZ会放到最后再写,因为LZ弄不懂。╮(╯▽╰)╭

除去Map一类,该包下的与数据结构相关的类主要有接口BlockingQueue和Deque。Deque实际上是一个双向链表的形式,大体和LinkedBlockingQueue类型。便不多加描述。本篇LZ主要写了BlockingQueue接口下的实现类:ArrayBlockingQueue和LinkedBlockingQueue。

BlockingQueue接口中主要的一些方法:
put:当队列已满,则当前线程会进入阻塞状态,知道队列中出现空的位置,知道队列中出现空的位置。
take:当队列为空时,当前线程会进入阻塞状态,知道队列中有数据存入。
offer:当队列中可以容纳时,返回true。反之返回false。但不会阻塞当前线程。
poll:取队列首位,不能取出时返回null。不会阻塞当前线程。

offer和poll还可以指定timeout,即当队列中不可容纳或者不可取出时,线程可等待指定的timeout。若在该时间段内可以获取或者继续存入,则返回true。若时间到后还不可获取或者存入,则返回false或null。

在BlockingQueue的源码中,有个关于生产者-消费者的官方示例,这一部分的类可用于实现生产者-消费者。

接下去看BlockingQueue的两个实现类ArrayBlockingQueue和LinkedBlockingQueue。


ArrayBlockingQueue

ArrayBlockingQueue继承了AbstractQueue,实现了BlockingQueue接口.其内部维护了一个定长的数组items。

成员变量:takeIndex(下一个可取到的元素的下标)、putIndex(下一个可存入的元素的下标)、count(队列中元素的个数)。接下来的三个变量是BlockingQueue实现同步的核心成员。ReentrantLock、Condition notEmpty、Condition notFull。

构造函数:形参capacity(指定数组items的容量),fair默认为false(不公平锁),如若为true,则按照FIFO获得锁资源。初始化notEmpty和notFull。

接下来便是其对于的BlockingQueue的主要方法的实现。
offer(E e)//同步,非阻塞:

先checkNotNull判断元素e是否为空。随后lock.lock。当队列中不能再容纳元素时,返回false。反之将元素e存入items。在enqueue方法中,会判断++putIndex是否等于数组的长度,如若等于则putIndex置零。即当存入e后putIndex到达队列末尾时。随后count++。唤醒notEmpty处的线程。

put(E e)//同步,阻塞:
这里使用的是lockInterruptibly,故而当线程在尝试获取锁的过程中被中断,则会取消获取锁。put首先会通过while循环判断count是否等于数组的长度,如若相等,则线程进入阻塞状态。否则进行入队列操作。

pull()//同步,非阻塞:
判断count是否为0,如若是则返回null,否则队首出队列。调用dequeue方法,会唤醒notFull处的线程。

take()//同步,阻塞:
也使用了lockInterruptibly,当队列为空时,会阻塞线程。否则返回队首。

可以看出ArrayBlockingQueue内的方法基本以同步实现,差别仅在于是否会阻塞线程。同时也可以发现所有的方法用的锁资源均为同一个lock,即共用锁对象。


LinkedBlockingQueue

LinkedBlockingQueue也同样继承了AbstrctQueue,实现了BlockingQueue。
其内部维护了一个链表结构,以静态内部类Node实现(成员变量有item,next,capacity,count,head和last)。
其包含了两种锁:takeLock(notEmpty)和putLock(notFull)

构造函数:无参默认长度为Integer.MAX_VALUE;当然也可指定capacity。随后初始化last和head为一个item为null的相同节点。

put(E e):贴上源码
首先putLock.lockInterruptibly。随后当count=capacity时,线程阻塞。反之元素存入链表尾部。随后再判断元素容量是否<capacity。如若是的话,则唤醒notFull处的线程。最后还有一步是要判断c是否为0,如若为0,则说明当前是第一次存入元素,队列中已存在元素,则要唤醒notEmpty处的线程。

take()//贴上源码

首先takeLock.lockInterruptibly。当count为0时,notEmpty阻塞线程。count不为0,则将队首移出。随后需要判断队列中是否还有元素,如若有,则唤醒notEmpty处线程。最后还需判断未取出元素前容量是否等于capacity,如若是,则说明这是第一次移出元素,队列存在空位置,需唤醒notFull处线程。

从上述分析中我们可以看出LinkedBlockingQueue采用了锁分离,读取和写入操作分别使用了不同的锁。这使得take和put无需实现同步操作。可在生产者-消费者模式下使用,但我们可以看出使用LinkedBlockingQueue会始终多出一个对象,即head/last。


DelayQueue:没有限制大小的队列。即插入数据不会被阻塞;而在读取数组时才会阻塞。
PriorityBlockingQueue:底层维护了数组,默认为11个长度。插入数据操作同样不收限制,只会阻塞读取操作。//采用公平锁
SynchronizedQueue:无缓冲的等待队列,synchronized关键字置于方法头。//采用class类作为锁。FILO

ConcurrentLinkedQueue:是一个并发队列,使用的是CAS实现同步机制。且该类的size回去遍历一遍链表,性能较慢。判断是否为空时建议用isEmpty。

ConcurrentLiknedQueue是一个并发队列,而LinkedBlockingQueue是一个缓冲队列。

ArrayBlockingQueue和LinkedBlockingQueue的区别:
1、ArrayBlockingQueue底层维护的是一个数组,而LinkedBlockingQueue底层维护的是链表结构。
2、ArrayBlockingQueue未实现锁分离,故而读/写操作不能同时进行。但LinkedBlockingQueue实现了读锁和写锁分离,性能高。
3、相较于ArrayBlockingQueue而言,LinkedBlockingQueue会额外多出一个对象。


CopyOnWriteArrayList和CopyOnWriteArraySet大体一致。这两种类的效率较低,适用于读多写少的情况。

首先回顾一下List和Set的区别:
1、List允许存储重复对象,而Set不允许。
2、List可保证顺序,即插入顺序;而Set不保证顺序,尤其是不能保证迭代的恒久不变。
3、List可存储Null元素,而Set只允许存储一个Null元素。
4、List在知道索引的情况下,获取元素的效率会更高。

由于这两者相差不大,CopyOnWriteArraySet底层维护的是CopyOnWriteArrayList,所以只单独说一下List。
CopyOnWriteArrayList在底层除了维护了一个array数组之外,还有一个ReentrantLock保证线程安全。
从字面意思理解,在每次进行写操作时,会将旧数组复制到一个新的数组,然后再覆盖原先的数组。

采用复制的原因主要是为了当旧列表还在被其他线程操作的时候保持有效。//读写分离【这个LZ是其他博主那看来的,不是很理解。】

猜你喜欢

转载自blog.csdn.net/qq_32302897/article/details/81038482