前言
在一篇分析AsyncTask源码中的文章中,我们看到了在线程并发中用到比较多的一个队列LinkedBlockingQueue,今天这篇文章就来分析下这个东西的使用
相关文章
从源码解析-Android数据结构ArrayBlockingQueue
从源码解析-Android数据结构之双向阻塞队列LinkedBlockingDeque的使用
从源码解析-Android数据结构之单向阻塞队列LinkedBlockingQueue的使用
从源码解析-Android数据结构之双端队列ArrayDeque
原理图
LinkedBlockingQueue:是concurrent包下的类,实现了BlockingQueue接口,是一种单向阻塞链表,是线程安全的队列,数据处理逻辑是先进先出,应该说是作为生产者消费者模型的首选;可以指定最大容量,也可以不指定最大容量,如果不指定的话,默认最大值是Integer.MAX_VALUE。如图
线程1不停的往队列尾部加入数据,线程2不停的从头部取出数据。
通过使用这种队列,我们可以放心的实现多线程操作,不需要自己开发,要处理各种对象锁,队列安全问题。
增加删除方法
LingkedBlockingQueue各有一套增加删除数据的方法
* <tr>
* <tr>
* <td><b>Insert</b></td>
* <td>{@link #add add(e)}</td>
* <td>{@link #offer offer(e)}</td>
* <td>{@link #put put(e)}</td>
* <td>{@link #offer(Object, long, TimeUnit) offer(e, time, unit)}</td>
* </tr>
* <tr>
* <td><b>Remove</b></td>
* <td>{@link #remove remove()}</td>
* <td>{@link #poll poll()}</td>
* <td>{@link #take take()}</td>
* <td>{@link #poll(long, TimeUnit) poll(time, unit)}</td>
* </t>
使用样例
现在我们通过代码来看看这两套方法如何操作。本例使用生产者消费者模型,构建一个苹果消费者,一个苹果生产者,一个装苹果的篮子,一个苹果对象。
苹果生产者
/**
* @Description TODO(苹果生产者)
* @author cxy
* @Date 2018/6/21 17:13
*/
public class Producer implements Runnable{
private String TAG = "Producer";
//生产者
private String produce;
//苹果篮子
private AppleBasket basket;
public Producer(String produce, AppleBasket basket) {
this.produce = produce;
this.basket = basket;
}
@Override
public void run() {
try {
for (int i = 0; ; i++) {
Apple bean = new Apple("苹果" + i + "号");
Log.e(TAG, produce + "正在生产苹果 " + bean);
basket.produce(bean, produce);
Log.e(TAG, produce + "生产苹果 " + bean + " 结束----------------");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
Log.e(TAG, "InterruptedException=" + e.getMessage());
e.printStackTrace();
} catch (IllegalStateException e) {
Log.e(TAG, "IllegalStateException=" + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
Log.e(TAG, "Exception=" + e.getMessage());
e.printStackTrace();
}
}
}
苹果消费者
/**
* @Description TODO(苹果消费者)
* @author cxy
* @Date 2018/6/21 17:14
*/
public class Consumer implements Runnable {
private String TAG = "Consumer";
private String consume;
private AppleBasket basket;
public Consumer(String consume, AppleBasket basket) {
this.consume = consume;
this.basket = basket;
}
@Override
public void run() {
try {
while (true) {
Log.e(TAG,consume+"正在消费苹果 ");
basket.consume(consume);
Log.e(TAG,consume+"消费苹果结束 -----------------------");
Thread.sleep(1000 * 20);
}
} catch (InterruptedException e) {
Log.e(TAG, "InterruptedException=" + e.getMessage());
e.printStackTrace();
} catch (IllegalStateException e) {
Log.e(TAG, "IllegalStateException=" + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
Log.e(TAG, "Exception=" + e.getMessage());
e.printStackTrace();
}
}
}
苹果篮子
/**
* @Description TODO(装苹果的篮子)
* @author cxy
* @Date 2018/6/21 17:03
*/
public class AppleBasket {
private String TAG = "AppleBasket";
BlockingQueue<Apple> queue = new LinkedBlockingQueue<>(10);
/**
* 生成苹果,放入队列
* @param bean
* @throws InterruptedException
*/
public void produce(Apple bean,String producer) throws InterruptedException {
//添加元素到队列,如果队列已满,线程进入等待,直到有空间继续生产
queue.put(bean);
//添加元素到队列,如果队列已满,抛出IllegalStateException异常,退出生产模式
// queue.add(bean);
//添加元素到队列,如果队列已满或者说添加失败,返回false,否则返回true,继续生产
// queue.offer(bean);
//添加元素到队列,如果队列已满,就等待指定时间,如果添加成功就返回true,否则false,继续生产
// queue.offer(bean,5, TimeUnit.SECONDS);
}
/**
* 消费苹果。从队列取出
* @return
* @throws InterruptedException
*/
public Apple consume(String consumer) throws InterruptedException {
//检索并移除队列头部元素,如果队列为空,线程进入等待,直到有新的数据加入继续消费
Apple bean = queue.take();
//检索并删除队列头部元素,如果队列为空,抛出异常,退出消费模式
// Apple bean = queue.remove();
//检索并删除队列头部元素,如果队列为空,返回false,否则返回true,继续消费
// Apple bean = queue.poll();
//检索并删除队列头部元素,如果队列为空,则等待指定时间,成功返回true,否则返回false,继续消费
// Apple bean = queue.poll(3, TimeUnit.SECONDS);
return bean;
}
}
苹果对象
/**
* @Description TODO(苹果类)
* @author cxy
* @Date 2018/6/21 17:04
*/
public class Apple {
private String name;
public Apple(String name) {
this.name = name;
}
@Override
public String toString() {
return name ;
}
}
现在来使用吧,在Activity里使用
AppleBasket basket = new AppleBasket();
ExecutorService service = Executors.newFixedThreadPool(5);
Producer producer = new Producer("生产者",basket);
Consumer consumer = new Consumer("消费者",basket);
service.execute(producer);
service.execute(consumer);
上面代码逻辑就是生产者每过一秒生产一个苹果,篮子采用put方法往队列里放苹果;消费者每过20秒消费一个苹果,篮子采用take方法从篮子取数据,队列的容量是只能放10个苹果;这样的情况就是当生产者生产了10个苹果以后,篮子就满了,但是此时消费者还没有开始消费苹果,那么这时候生产者就会阻塞住,也就是生产线程被wait了,一直到20s后消费者开始消费苹果了,这时候生产线程就被唤醒了,就继续开始生产苹果,知道队列满了再继续被阻塞。我们看打印的日志
06-22 10:39:46.778 25997-26045/? E/Producer: 生产者正在生产苹果 苹果0号
06-22 10:39:46.778 25997-26045/? E/Producer: 生产者生产苹果 苹果0号 结束----------------
06-22 10:39:47.778 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果1号
06-22 10:39:47.779 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果1号 结束----------------
06-22 10:39:48.779 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果2号
06-22 10:39:48.779 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果2号 结束----------------
06-22 10:39:49.779 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果3号
06-22 10:39:49.779 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果3号 结束----------------
06-22 10:39:50.779 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果4号
06-22 10:39:50.780 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果4号 结束----------------
06-22 10:39:51.780 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果5号
06-22 10:39:51.780 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果5号 结束----------------
06-22 10:39:52.780 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果6号
06-22 10:39:52.780 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果6号 结束----------------
06-22 10:39:53.781 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果7号
06-22 10:39:53.781 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果7号 结束----------------
06-22 10:39:54.781 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果8号
06-22 10:39:54.781 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果8号 结束----------------
06-22 10:39:55.781 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果9号
06-22 10:39:55.782 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果9号 结束----------------
06-22 10:39:56.782 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果10号
06-22 10:40:06.778 25997-26046/com.android.mangodialog E/Consumer: 消费者正在消费苹果
06-22 10:40:06.778 25997-26046/com.android.mangodialog E/Consumer: 消费者消费苹果结束 -----------------------
06-22 10:40:06.778 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果10号 结束----------------
当生产者生产到第10号苹果的时候,队列已经放了10个苹果,这时候线程就被阻塞了,直到消费者消费了一个苹果以后,生产者才生产成功了一个苹果。
如果我们把逻辑改下,生产者每20s生产一个苹果,消费者每过1s消费一个苹果,那么情况就是消费者就会被堵塞住,当20s的时候生产者生产了一个苹果,消费者就开始成功消费一个苹果,然后就继续被阻塞了,知道下一个苹果被成功生产。
源码解析
我们看看LinkedBlockingQueue的源码,前面说了它是一个单向链表阻塞队列,看看源码中的节点类
节点类
/**
* Linked list node class.
*/
static class Node<E> {
E item;
/**
* One of:
* - the real successor Node
* - this Node, meaning the successor is head.next
* - null, meaning there is no successor (this is the last node)
*/
Node<E> next;
Node(E x) { item = x; }
}
节点类只维护了后一个节点,没有前一个节点(后面讲到LinkedBlockingDeque时会讲到两个节点),这就是它的单向链表由来。
变量
再看看这个类里面定义的变量
/** 链表容量,如果没有指定,设置为Integer.MAX_VALUE */
private final int capacity;
/** 链表当前元素数量 */
private final AtomicInteger count = new AtomicInteger();
/**
* Head of linked list.
* Invariant: head.item == null
* 队列头部节点,并且头部节点中的元素为null
*/
transient Node<E> head;
/**
* Tail of linked list.
* Invariant: last.next == null
* 队列尾部节点,并且尾部节点后面的一个节点为null
*/
private transient Node<E> last;
/** Lock held by take, poll, etc
* 用于take、poll等方法将元素出队的锁
* */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes
* 当队列为空时,保存出队线程
* */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc
* 用于put、offer等方法将元素入队的锁
* */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts
* 当队列满的时候,保存入队的线程
* */
private final Condition notFull = putLock.newCondition();
看到这里定义了两把锁,一把控制出队线程,一把控制入队线程,这说明了同一时间只能有一个线程出队,一个线程入队,其余线程都会被阻塞;并且用了AtomicInteger 来表示队列元素个数,这是原子操作的Integer,也就是每次都是来读取这个真正的内存中的值,而不是去读缓存,也就能保证出队线程和入队线程在操作队列时是安全的。
构造方法
再看接下来构造方法
/**
* Creates a {@code LinkedBlockingQueue} with a capacity of
* {@link Integer#MAX_VALUE}.
* 空的构造方法,默认设置队列长度为Integer.MAX_VALUE
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
/**
* Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
*
* @param capacity the capacity of this queue
* @throws IllegalArgumentException if {@code capacity} is not greater than zero
*
* 传入一个队列长度值,并且实例化了一个头部节点和尾部节点,且节点内的元素为null
*/
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
/**
* Creates a {@code LinkedBlockingQueue} with a capacity of
* {@link Integer#MAX_VALUE}, initially containing the elements of the
* given collection,
* added in traversal order of the collection's iterator.
*
* @param c the collection of elements to initially contain
* @throws NullPointerException if the specified collection or any
* of its elements are null
*
* 传入一个集合
*/
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
// Never contended, but necessary for visibility
//获取队列入队锁,虽然这时候没人竞争锁,但是有必要锁住
putLock.lock();
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
//将元素入队
enqueue(new Node<E>(e));
//将元素计数
++n;
}
//设置队列长度
count.set(n);
} finally {
//释放锁
putLock.unlock();
}
}
这里有一个必走的构造方法就是第二个,做了两件事,指定队列长度,实例化一个头部节点和一个尾部节点,不过节点内的元素为null。
put操作和take操作
看看put操作和take操作
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
//以e创建节点
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
//获取入队锁
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
* 如果队列满了,就将线程放入到Condition等待队列中
*/
while (count.get() == capacity) {
notFull.await();
}
//将节点放入到队列中
enqueue(node);
//获取当前队列长度,然后将count加一
c = count.getAndIncrement();
//如果队列没有满,就通知其它的入队线程
if (c + 1 < capacity)
notFull.signal();
} finally {
//释放入队锁
putLock.unlock();
}
//如果队列长度从0到非0,那就通知出队线程来取数据
if (c == 0)
signalNotEmpty();
}
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
//获取出队锁
takeLock.lockInterruptibly();
try {
//如果队列是空的,就将线程放入到Condition等待队列中
while (count.get() == 0) {
notEmpty.await();
}
//取出一个节点元素
x = dequeue();
//获取取出之前的队列长度
c = count.getAndDecrement();
//如果队列不为空,通知Condition等待队列中的线程来取数据
if (c > 1)
notEmpty.signal();
} finally {
//释放锁
takeLock.unlock();
}
//如果队列长度从满到非满,通知入队线程生产数据
if (c == capacity)
signalNotFull();
return x;
}
/**
* Signals a waiting take. Called only from put/offer (which do not
* otherwise ordinarily lock takeLock.)
* 通知出队线程,队列已经不为空了,可以来获取元素了
*/
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
//获取元素出队的锁
takeLock.lock();
try {
//释放出队线程的第一个等待线程
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
/**
* Signals a waiting put. Called only from take/poll.
* 通知入队线程,队列没有满,可以来添加元素了
*/
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
//获取元素入队的锁
putLock.lock();
try {
//释放入队线程的第一个线程
notFull.signal();
} finally {
putLock.unlock();
}
}
/**
* Links node at end of queue.
* 创建一个节点,添加到链表尾部
* @param node the node
*/
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
//封装新节点,并赋给当前的最后一个节点的下一个节点,然后将这个节点设为最后一个节点
last = last.next = node;
}
/**
* Removes a node from head of queue.
* 从队列头部移除一个节点
* @return the node
*/
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
//获取头节点 元素为null
Node<E> h = head;
//将头节点的下一个节点赋值给first
Node<E> first = h.next;
//将当前将要出队的节点置为null,为了使其做head节点做准备
h.next = h; // help GC
//将当前要出队的节点作为头节点
head = first;
//获取出队节点的元素值
E x = first.item;
//将出队节点的元素值置为null
first.item = null;
return x;
}
注释描述的很清楚了,其实最主要的是要理解出队dequeue和入队enqueue两个方法的逻辑
节点next
首先我们来看下节点类中的next的这段注释
/**
* One of:
* - the real successor Node
* - this Node, meaning the successor is head.next
* - null, meaning there is no successor (this is the last node)
*/
Node<E> next;
也就是说当前节点的下一个节点才是我们真正去操作的节点,如果这个节点的下一个节点为null,那这个节点就是最后一个节点;
这可以看出head.item=null,last.next = null
还有一个意思其实是head这个节点从初始化的时候就能看出内部元素为null,如下图
当我们调用构造方法的时候,内部有这么一句代码
last = head = new Node(null);
就是将last引用和head引用都指向了这个new的节点,元素为null
接下来就是我们调用put方法添加节点的时候了,从上面代码可以看出,最终会走到enqueue这个方法,就一句代码
last = last.next = node;
先将新封装的节点引用赋值给了last.next;但是这时候last的引用还是和head一样指向第一个节点(这时候的last.next和head.next是一样的),所以这时候第二个等号将last.next的引用又赋值给了last
然后调用take方法出队,看看出队逻辑
//获取头节点 元素为null
Node<E> h = head;
//将头节点的下一个节点赋值给first
Node<E> first = h.next;
//将当前将要出队的节点指到head节点的位置
h.next = h; // help GC
//将当前要出队的节点作为头节点
head = first;
//获取出队节点的元素值
E x = first.item;
//将出队节点的元素值置为null
first.item = null;
return x;
执行完第一句和第二句代码后
逻辑就是:将头节点的引用给到了h,将新增加的节点的引用给到了first,接下来执行最后的代码
第三步:
将第二个节点也就是要出队的节点指向head节点,
第四步:
将当前要出对的节点引用赋值给head;这一步走完,存在堆内存中的第一个节点就没有全局变量的引用指向它
第五步:
把要出队的节点的元素取出来赋值给x
第五步:
将第二个节点元素置为null,然后将元素返回
当这个方法执行完毕,最开始的第一个节点没有全局变量引用指向它,方法内的引用会从栈内回收,第一个节点的堆内存最终也会被回收,而第二个节点中的元素置为null,就这样它变成了第一个节点,同样的只有两个全局变量的引用head和last指向它。