之前犯了一个错误,一直以为ArrayBlcokingQueue用的是单向链表,LinkedBlockingQueue用的是双向链表。这是一个错误的理解,在此要纠正下。
ArrayBlcokingQueue与LinkedBlockingQueue在自定义线程池的时候经常用的两种队列模式。
ArrayBlcokingQueue是 基于数组的先进先出阻塞队列。
LinkedBlockingQueue是基于单向链表的先进先出阻塞队列。
在实际的业务场景中我们一般会碰到以下四种场景(在对同一个数据进行操作时)
写写 (不可并行)
写读 (?)
读读 (可并行)
读写 (?)
为什么读写和写读这里打个问号呢?这里我也不好回答究竟可不可以并行。在innoDB数据库中读写,写读是可以并行的。数据库中实现了MVCC(多版本控制),在每次进行操作的时候都会找跟版本一致快照数据,进行读操作。从而实现写阻塞读。
这里大家可以去看看mvcc与copy on write 的解释。
但是在java程序中,我们是不可能去实现mvcc机制的。所以写读与读写可不可以并行,要根据具体的情况进行设计。
那么LinkedBlockingQueue,写读读写是可以并行的。ArrayBlcokingQueue是不可以并行,具体怎么实现的请接着往下看。
LinkedBlockingQueue采用链表结构,其内部实现了添加和删除操作使用的两个ReenterLock(可重入)来控制并发执行。
ArrayBlcokingQueue是基于数组的队列(从队头获取在队尾放入),其内部只实现了一个ReenterLock来控制并发执行。数组的一个问题就是位置的选择没办法原子化,因为位置会移动,走到最后一个位置后就返回到第一个位置。每次对数组进行添加或删除的操作时都会设计到数组位置的移动,移动的操作无法原子化,所以直接采用一个大锁把数组锁住。
那为什么链表可以实现读写写读并行呢呢?
链表与数组的结构不同,链表队列的添加和头部的删除都只和一个节点相关,不需要移动位置。添加只往后加就可以了删除只从头部去掉就好。为了防止head和tail相互影响出现问题,这里就引入了一个原子性的计数器。头部需要移除的时候,首先要看计数器的值是否大于0,大于0就可以删除,并且把计数器的值-1.。每个添加操作都是先加入队列,然后计数器+1。这样就保证了,队列在移除的时候,长度是大于等于计数器的,通过原子性的计数器,双锁实现了互不干扰。
下面我们来看看put与take方法的实现。
这个是LinkedBlockingQueue的内部成员变量
LinkedBlockingQueue初始化的构造方法
Put方法
take方法