深圳网站建设运营公司,.net网站封装,手机开发者选项开启的好还是关闭的好,网站建设平台推荐转载自 并发队列-无界非阻塞队列 ConcurrentLinkedQueue 原理探究一、 前言
常用的并发队列有阻塞队列和非阻塞队列#xff0c;前者使用锁实现#xff0c;后者则使用CAS非阻塞算法实现#xff0c;使用非阻塞队列一般性能比较好#xff0c;下面就看看常用的非阻塞Concurrent…转载自 并发队列-无界非阻塞队列 ConcurrentLinkedQueue 原理探究一、 前言
常用的并发队列有阻塞队列和非阻塞队列前者使用锁实现后者则使用CAS非阻塞算法实现使用非阻塞队列一般性能比较好下面就看看常用的非阻塞ConcurrentLinkedQueue是如何使用CAS实现的。
二、 ConcurrentLinkedQueue类图结构如图ConcurrentLinkedQueue中有两个volatile类型的Node节点分别用来存在列表的首尾节点其中head节点存放链表第一个item为null的节点tail则并不是总指向最后一个节点。Node节点内部则维护一个变量item用来存放节点的值next用来存放下一个节点从而链接为一个单向无界列表。
123public ConcurrentLinkedQueue() { head tail new NodeE(null);}如上代码初始化时候会构建一个item为NULL的空节点作为链表的首尾节点。
三、offer操作
offer操作是在链表末尾添加一个元素下面看看实现原理。
12345678910111213141516171819202122232425262728293031323334public boolean offer(E e) { //e为null则抛出空指针异常 checkNotNull(e); //构造Node节点构造函数内部调用unsafe.putObject后面统一讲 final NodeE newNode new NodeE(e); //从尾节点插入 for (NodeE t tail, p t;;) { NodeE q p.next; //如果qnull说明p是尾节点则插入 if (q null) { //cas插入1 if (p.casNext(null, newNode)) { //cas成功说明新增节点已经被放入链表然后设置当前尾节点包含head135.。。个节点为尾节点 if (p ! t) // hop two nodes at a time casTail(t, newNode); // Failure is OK. return true; } // Lost CAS race to another thread; re-read next } else if (p q)//(2) //多线程操作时候由于poll时候会把老的head变为自引用然后head的next变为新head所以这里需要 //重新找新的head因为新的head后面的节点才是激活的节点 p (t ! (t tail)) ? t : head; else // 寻找尾节点(3) p (p ! t t ! (t tail)) ? t : q; }}从构造函数知道一开始有个item为null的哨兵节点并且head和tail都是指向这个节点然后当一个线程调用offer时候首先如图首先查找尾节点qnull,p就是尾节点所以执行p.casNext通过cas设置p的next为新增节点这时候pt所以不重新设置尾节点为当前新节点。由于多线程可以调用offer方法所以可能两个线程同时执行到了1进行cas那么只有一个会成功假如线程1成功了成功后的链表为失败的线程会循环一次这时候指针为这时候会执行3所以pq,然后在循环后指针位置为所以没有其他线程干扰的情况下会执行1执行cas把新增节点插入到尾部没有干扰的情况下线程2 cas会成功然后去更新尾节点tail,由于p!t所以更新。这时候链表和指针为假如线程2cas时候线程3也在执行那么线程3会失败循环一次后线程3的节点状态为这时候p!t 并且t的原始值为toldt的新值为tnew ,所以told!tnew所以 ptnewtail;
然后在循环一下后节点状态:qnull所以执行1。
现在就差pq这个分支还没有走这个要在执行poll操作后才会出现这个情况。poll后会存在下面的状态这个时候添加元素时候指针分布为所以会执行2分支 结果 phead然后循环循环后指针分布所以执行(1),然后p!t所以设置tail节点。现在分布图自引用的节点会被垃圾回收掉。
四、 add操作
add操作是在链表末尾添加一个元素下面看看实现原理。其实内部调用的还是offer
123public boolean add(E e) { return offer(e);}五、poll操作
poll操作是在链表头部获取并且移除一个元素下面看看实现原理。
123456789101112131415161718192021222324252627282930313233343536public E poll() { restartFromHead: //死循环 for (;;) { //死循环 for (NodeE h head, p h, q;;) { //保存当前节点值 E item p.item; //当前节点有值则cas变为null1 if (item ! null p.casItem(item, null)) { //cas成功标志当前节点以及从链表中移除 if (p ! h) // 类似tail间隔2设置一次头节点2 updateHead(h, ((q p.next) ! null) ? q : p); return item; } //当前队列为空则返回null3 else if ((q p.next) null) { updateHead(h, p); return null; } //自引用了则重新找新的队列头节点4 else if (p q) continue restartFromHead; else//(5) p q; } }} final void updateHead(NodeE h, NodeE p) { if (h ! p casHead(h, p)) h.lazySetNext(h); }当队列为空时候可知执行3这时候有两种情况第一没有其他线程添加元素时候(3)结果为true然后因为h!p为false所以直接返回null。第二在执行qp.next前其他线程已经添加了一个元素到队列这时候3返回false然后执行5pq,然后循环后节点分布这时候执行1分支进行cas把当前节点值值为null同时只有一个线程会成功cas成功 标示该节点从队列中移除了然后p!h,调用updateHead方法参数为h,p;h!p所以把p变为当前链表head节点然后h节点的next指向自己。现在状态为cas失败 后 会再次循环这时候分布图为这时候执行3返回null.
现在还有个分支4没有执行过那么什么时候会执行那这时候执行1分支进行cas把当前节点值值为null同时只有一个线程A会成功cas成功 标示该节点从队列中移除了然后p!h,调用updateHead方法假如执行updateHead前另外一个线程B开始poll这时候它p指向为原来的head节点然后当前线程A执行updateHead这时候B线程链表状态为所以会执行4重新跳到外层循环获取当前head,现在状态为六、peek操作
peek操作是获取链表头部一个元素只读取不移除下面看看实现原理。代码与poll类似只是少了castItem.并且peek操作会改变head指向offer后head指向哨兵节点第一次peek后head会指向第一个真的节点元素。
12345678910111213141516public E peek() { restartFromHead: for (;;) { for (NodeE h head, p h, q;;) { E item p.item; if (item ! null || (q p.next) null) { updateHead(h, p); return item; } else if (p q) continue restartFromHead; else p q; } }}七、size操作
获取当前队列元素个数在并发环境下不是很有用因为使用CAS没有加锁所以从调用size函数到返回结果期间有可能增删元素导致统计的元素个数不精确。
123456789101112131415161718192021222324252627282930313233public int size() { int count 0; for (NodeE p first(); p ! null; p succ(p)) if (p.item ! null) // 最大返回Integer.MAX_VALUE if (count Integer.MAX_VALUE) break; return count;}//获取第一个队列元素哨兵元素不算没有则为nullNodeE first() { restartFromHead: for (;;) { for (NodeE h head, p h, q;;) { boolean hasItem (p.item ! null); if (hasItem || (q p.next) null) { updateHead(h, p); return hasItem ? p : null; } else if (p q) continue restartFromHead; else p q; } }}//获取当前节点的next元素如果是自引入节点则返回真正头节点final NodeE succ(NodeE p) { NodeE next p.next; return (p next) ? head : next;}八、remove操作
如果队列里面存在该元素则删除给元素如果存在多个则删除第一个并返回true否者返回false
12345678910111213141516171819202122232425public boolean remove(Object o) { //查找元素为空直接返回false if (o null) return false; NodeE pred null; for (NodeE p first(); p ! null; p succ(p)) { E item p.item; //相等则使用cas值null,同时一个线程成功失败的线程循环查找队列中其他元素是否有匹配的。 if (item ! null o.equals(item) p.casItem(item, null)) { //获取next元素 NodeE next succ(p); //如果有前驱节点并且next不为空则链接前驱节点到next, if (pred ! null next ! null) pred.casNext(p, next); return true; } pred p; } return false;}九、contains操作
判断队列里面是否含有指定对象由于是遍历整个队列所以类似size 不是那么精确有可能调用该方法时候元素还在队列里面但是遍历过程中才把该元素删除了那么就会返回false.
123456789public boolean contains(Object o) { if (o null) return false; for (NodeE p first(); p ! null; p succ(p)) { E item p.item; if (item ! null o.equals(item)) return true; } return false;}十、开源框架中使用
Tomcat中NioEndPoint中的每个poller里面就维护一个ConcurrentLinkedQueueRunnable用来作为缓冲存放任务。
10.1 Acceptor线程
accept线程作用是接受客户端发来的连接请求并放入到事件队列。看下代码
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071protected class Acceptor extends AbstractEndpoint.Acceptor { Override public void run() { int errorDelay 0; // 一直循环直到接收到shutdown命令 while (running) { ... if (!running) { break; } state AcceptorState.RUNNING; try { //如果达到max connections个请求则等待 countUpOrAwaitConnection(); SocketChannel socket null; try { // 从TCP缓存获取一个完成三次握手的套接字没有则阻塞 // socket socket serverSock.accept(); } catch (IOException ioe) { ... } // Successful accept, reset the error delay errorDelay 0; if (running !paused) { if (!setSocketOptions(socket)) { countDownConnection(); closeSocket(socket); } } else { countDownConnection(); closeSocket(socket); } .... } catch (SocketTimeoutException sx) { // Ignore: Normal condition .... } state AcceptorState.ENDED; } } protected boolean setSocketOptions(SocketChannel socket) { // Process the connection try { //disable blocking, APR style, we are gonna be polling it ... getPoller0().register(channel); } catch (Throwable t) { ... return false; } return true;}public void register(final NioChannel socket) { ... addEvent(r);}public void addEvent(Runnable event) { events.offer(event); ...}10.2 Poll线程
poll线程作用是从事件队列里面获取事件把链接套接字加入selector,并且监听socket事件进行处理。12345678910111213141516171819202122232425262728293031323334353637383940414243public void run() { while (true) { try { ... if (close) { ... } else { hasEvents events(); } try { ... } catch ( NullPointerException x ) {... } IteratorSelectionKey iterator keyCount 0 ? selector.selectedKeys().iterator() : null; // 遍历所有注册的channel对感兴趣的事件处理 while (iterator ! null iterator.hasNext()) { SelectionKey sk iterator.next(); KeyAttachment attachment (KeyAttachment)sk.attachment(); if (attachment null) { iterator.remove(); } else { attachment.access(); iterator.remove(); processKey(sk, attachment); } }//while //process timeouts timeout(keyCount,hasEvents); if ( oomParachute 0 oomParachuteData null ) checkParachute(); } catch (OutOfMemoryError oom) { ... } }//while synchronized (this) { this.notifyAll(); } stopLatch.countDown();}123456789101112131415161718192021222324252627282930313233343536373839public boolean events() { boolean result false; //从队列获取任务并执行 Runnable r null; while ( (r events.poll()) ! null ) { result true; try { r.run(); if ( r instanceof PollerEvent ) { ((PollerEvent)r).reset(); eventCache.offer((PollerEvent)r); } } catch ( Throwable x ) { log.error(,x); } } return result; }//如配置线程池则请求交给线程池处理。public boolean processSocket(NioChannel socket, SocketStatus status, boolean dispatch) { try { KeyAttachment attachment (KeyAttachment)socket.getAttachment(); if (attachment null) { return false; } attachment.setCometNotify(false); //will get reset upon next reg SocketProcessor sc processorCache.poll(); if ( sc null ) sc new SocketProcessor(socket,status); else sc.reset(socket,status); if ( dispatch getExecutor()!null ) getExecutor().execute(sc); else sc.run(); } catch (RejectedExecutionException rx) { ... } return true;}十一、有意思的问题
10.1 一个判断的执行结果分析
offer中有个 判断 t ! (t tail假如 tnode1;tailnode2;并且node1!node2那么这个判断是true还是false那答案是true这个判断是看当前t是不是和tail相等相等则返回true否者为false但是无论结果是啥执行后t的值都是tail。
下面从字节码来分析下为啥
一个例子
123456789public static void main(String[] args) { int t 2; int tail 3; System.out.println(t ! (t tail));}结果为true;
字节码文件字节码命令介绍可参考 http://blog.csdn.net/web_code/article/details/12164733
一开始栈为空第0行指令作用是把值2入栈栈顶元素为2第1行指令作用是将栈顶int类型值保存到局部变量t中。第2行指令作用是把值3入栈栈顶元素为3第3行指令作用是将栈顶int类型值保存到局部变量tail中。第4调用打印命令第7行指令作用是把变量t中的值入栈第8行指令作用是把变量tail中的值入栈现在栈里面元素为32并且3位栈顶第9行指令作用是当前栈顶元素入栈所以现在栈内容332第10行指令作用是把栈顶元素存放到t现在栈内容32第11行指令作用是判断栈顶两个元素值相等则跳转 18。由于现在栈顶严肃为32不相等所以返回true.第14行指令作用是把1入栈。
然后回头分析下!是双目运算符应该是首先把左边的操作数入栈然后在去计算了右侧操作数。
10.2 Node的构造函数
另外对于每个节点Node在构造时候使用UnSafe.putObject设置item替代了直接对volatile的赋值这个是为了性能考虑为啥不直接赋值那看看类注解怎么说
123Node(E item) { UNSAFE.putObject(this, itemOffset, item);}When constructing a Node (before enqueuing it) we avoid paying for a volatile write to item by using Unsafe.putObject instead of a normal write. This allows the cost of enqueue to be”one-and-a-half”CASes.
也就是说当构造Node节点时候这时候节点还没有放入队列链表为了避免正常的写volatile变量的开销 使用了Unsafe.putObject代替。这使元素进队列仅仅花费1.5个cas操作的耗时。这个是说使用Unsafe.putObject比直接给volatile变量赋值更高效目前还没有查到相关资料。
十二、总结
ConcurrentLinkedQueue使用CAS非阻塞算法实现使用CAS解决了当前节点与next节点之间的安全链接和对当前节点值的赋值。由于使用CAS没有使用锁所以获取size的时候有可能进行offerpoll或者remove操作导致获取的元素个数不精确所以在并发情况下size函数不是很有用。另外第一次peek或者first时候会把head指向第一个真正的队列元素。
下面总结下如何实现线程安全的可知入队出队函数都是操作volatile变量headtail。所以要保证队列线程安全只需要保证对这两个Node操作的可见性和原子性由于volatile本身保证可见性所以只需要看下多线程下如果保证对着两个变量操作的原子性。
对于offer操作是在tail后面添加元素也就是调用tail.casNext方法而这个方法是使用的CAS操作只有一个线程会成功然后失败的线程会循环一下重新获取tail然后执行casNext方法。对于poll也是这样的。