并发基础_11_并发_容器_ConcurrentLinkedQueue

要实现一个线程安全的队列有两个方式:一种是使用阻塞算法,另一种是使用非阻塞算法。


阻塞算法:

使用阻塞算法的队列可以用一个锁(入队和出队同一把锁)或两把锁(入队和出队用不同的锁)来实现。

非阻塞的实现方式则可以使用循环CAS的方式来实现。



ConcurrentLinkedQueue非阻塞线程安全队列

ConcurrentLinkedQueue是一个基于链接节点的无界 线程安全的队列

它采用FIFO(先进先出)的规则对节点进行排序;

当我们添加一个元素的时候,它会添加到队列的尾部;

当我们获取一个元素时,它会返回队列头部的元素。

队列中不允许null元素。

判断队列成员是否为空,不要使用size()方法,使用isEmpty()方法,因为size()方法会遍历整个队列。


ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成;

节点与节点之间就是通过这个next关联起来,从而形成一个链表结构的队列。

我们看下源码中的参数:

private transient volatile Node<E> head;

private transient volatile Node<E> tail;

public ConcurrentLinkedQueue() {
	head = tail = new Node<E>(null);
}

要是你看过HahsMap的源码,会发现,这个和Entry链表差不多..

默认情况下head节点存储的元素为空,tail节点等于head节点

我们看下Node的源码:

private static class Node<E> {
	volatile E item;
	volatile Node<E> next;

	/**
	 * Constructs a new node. Uses relaxed write because item can only be
	 * seen after publication via casNext.
	 */
	Node(E item) {
			UNSAFE.putObject(this, itemOffset, item);
	}

	boolean casItem(E cmp, E val) {
			return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
	}

	void lazySetNext(Node<E> val) {
			UNSAFE.putOrderedObject(this, nextOffset, val);
	}

	boolean casNext(Node<E> cmp, Node<E> val) {
			return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
	}
	.....
}



ConcurrentLinkedQueue入队操作

入队列就是将新添加的Node节点,添加到队列的尾部。

在没添加元素之前,head和tail都是Node自己(head节点)。

a. 添加元素1:队列更新head节点的next节点为"元素1节点",又因为tail节点默认情况下等于head节点,所以他们的next节点都指向"元素1节点"

b. 添加元素2:队列首先设置"元素节点1"的next节点为"元素2节点",然后更新tail节点指向"元素2节点"

c. 添加元素3:设置tail节点为元素3节点

d. 添加元素4:设置元素3的next节点为元素4节点,然后将tail节点指向元素4节点。


通过观察入队过程以及head节点和tail节点的变化,发现入队主要做两件事:

第一:将入队节点设置成当前队列尾节点的下一个节点。

第二:更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成tail节点,如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点。(理解这点很重要...)


(让tail节点永远作为队列的尾节点,这样实现的代码量非常少,而且逻辑清晰、易懂,但是有个缺点,每次都要使用循环CAS更新tail节点。

如果能减少CAS更新tail节点的次数,就能提高入队的效率;so,并不是每次节点入队后都将tail节点更新成尾节点;而且随着队列长度越来越大,

每次入队时定位尾节点的时间就越长,太消耗性能..)

我们来看下源码:

public boolean offer(E e) {

	// 检查传入参数是否为空
	checkNotNull(e);

	// 构造一个新的Node节点,入队节点
	final Node<E> newNode = new Node<E>(e);

	// t为tail节点,p为尾节点,默认相等,采用失败即重试的方式,直到入队成功
	for (Node<E> t = tail, p = t;;) {

	// 获取p的下一个节点q
	Node<E> q = p.next;

	// 如果q为null(p就是尾节点)
	if (q == null) {

	// p is last node,p是尾节点
	// 将入队节点newNode,设置为当前队列尾节点的next节点
	if (p.casNext(null, newNode)) {

	// 判断tail节点是不是尾节点,也可以理解为如果插入节点后,tail节点与p节点距离是否达到两个节点?
	if (p != t) // hop two nodes at a time

	// 如果tail不是尾节点,则将入队列节点(newNoed)设置为tail节点。
	casTail(t, newNode); // Failure is OK.
	return true;
	}
	}
	// 如果p和它的下一个节点相等。
	// 则说明p节点和q节点都为空,表示队列刚刚初始化,所以返回head节点
	else if (p == q)
			p = (t != (t = tail)) ? t : head;
	else
			// p有next节点,表示p的next节点是尾节点,则需要重新更新p后,将它指向next节点
			p = (p != t && t != (t = tail)) ? t : q;
	}
}
(此源码来自JDK1.7,与原书有差别)

从源码角度来看,整个入队过程主要做两件事:


1. 定位出尾节点;

tail节点并不总是尾节点,所以每次入队都必须通过tail节点来找到尾节点。

尾节点可能是tail节点也可能是tail节点的next节点


2. 使用CAS算法将入队节点设置成尾节点的next节点,如不成功则重试。



ConcurrentLinkedQueue出队列

出队列就是从队列中返回一个节点元素,并清空该节点对元素的引用。

观察上图可以发现,并不是每次出队列时都更新head节点,当head节点有元素时,直接弹出head节点中的元素,而不会更新head节点。

只有当head节点中没有时,出队操作才会更新head节点。

主要是,尽量减少CAS更新head节点的消耗,这种做法可以提高出队的效率。



ConcurrentLinkedQueue使用Demo

public class ConcurrentLinkedQueueTest {

	private static ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<Integer>();

	private static int count = 2;

	private static CountDownLatch latch = new CountDownLatch(count);

	public static void main(String[] args) throws Exception {

	long startTime = System.currentTimeMillis();
	ExecutorService service = Executors.newFixedThreadPool(4);

	ConcurrentLinkedQueueTest.offer();

	for (int i = 0; i < count; i++) {
			service.submit(new Pool());
	}
	latch.await();
	System.out.println("cost time : " + (System.currentTimeMillis() - startTime) + " ms");

	service.shutdown();

	}

	/**
	 * 生产者
	 */
	public static void offer() {
			for (int i = 0; i < 10000; i++) {
					queue.offer(i);
			}
	}

	/**
	 * 消费
	 * 
	 * @author CYX
	 * @time 2017年7月31日上午9:34:02
	 */
	static class Pool implements Runnable {

	@Override
	public void run() {
			// 此处判断队列是否为空,不要使用size(),size()会将队列先遍历一遍,性能太差
			while (!queue.isEmpty()) {
					System.out.println("消费 : " + queue.poll());
			}
			latch.countDown();
	}

	}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值