上一节咱们以计数器做为例子描述了非阻塞算法,这一节咱们拿一个稍微复杂一点的数据结构栈来说述非阻塞算法的实践应用。
1.单线程栈java
public class SingleThreadStack implements Stack{ private Node head; public Node pop() { if (head == null) { return null; } Node h = head; head = head.getNext(); h.setNext(null); return h; } @Override public void push(int value) { Node node = new Node(); node.setValue(value); node.setNext(head); head = node; } }
出栈即把栈头弹出,而且把栈头设为下一个节点,若是栈头为空,说明栈是空的,直接返回null。入栈时把新入栈节点的next设为原来的栈头,而且把新的节点设为栈头。这个栈不是线程安全的,拿出栈来讲,若是A、B两个线程同时出栈,栈中只有2个元素,两次出栈后栈内元素应该都弹出了,可是若是A、B两个线程同时执行了head = head.getNext();这条代码(这条代码本质是先取出next,再赋值)就可能使得A、B两个线程两次出栈操做只出弹出并返回了同一个元素。入栈也是相同的道理。node
2.多线程同步栈算法
public class SynchronizedStack implements Stack { private SingleThreadStack delegate = new SingleThreadStack(); @Override public synchronized Node pop() { return delegate.pop(); } @Override public synchronized void push(int value) { delegate.push(value); } }
为了更好地说明同步的做用,这里直接把全部栈操做代理给了前文的单线程栈,能够看到由于出栈和入栈都是同步串行操做,就不会出现前面说起的多线程并发操做致使的两个线程同时操做只出了一个元素的状况。实际是把多线程并发操做排了个队,先拿到锁的先操做,后拿到锁的后操做。安全
3.无锁算法栈数据结构
public class CasNoLockStack implements Stack { private CasNode head; @Override public CasNode pop() { for (; ; ) { CasNode h = head; // 当前栈是空的 if (h == null) { return null; } CasNode next = h; if (head.casSet(h, next)) { h.setNext(null); return h; } } } @Override public void push(int value) { CasNode node = new CasNode(); node.setValue(value); for (; ; ) { CasNode h = head; if (head.casSet(h, node)) { node.setNext(h); return; } } } }
分别从出栈和入栈来讲:
出栈时,先在当前线程获取栈头赋值给局部变量h,若是h为空,说明当前栈为空栈,直接返回null便可。继续获取h的下一个节点next,而后尝试把栈头用cas(h,next)方法设置为next节点,这个方法若是返回成功的话,说明已经成功的更新栈头,那说明h节点是以前最新的栈头,将h的next节点设置为空并返回便可。这个方法若是返回失败的话,说明栈头已经被其余线程修改了,那就从新从最开始的第一步开始循环这个过程直到成功为止,这个循环必定会结束,由于cas操做不成功表明其余线程已经作了出栈操做,那最后要么成功在当前线程出栈,要么其余线程把栈内元素都弹出,当前线程返回null。
接下来讲入栈,入栈时先建立新的栈头节点node,进入循环后,第一步先获取当前栈头赋值给局部变量h,用cas(h,node)尝试把栈头赋值给新的入栈节点node,若是成功的话说明其余线程没有修改栈头,当前线程已经修改栈头成功,再把新栈头的next指向旧的栈头便可,若是失败的话说明其余线程已经修改了栈头节点,须要从新循环回到第一步继续获取新的栈头进行操做直到成功为止。在竞争十分严重的状况下,可能会失败屡次,执行屡次循环。多线程