学习笔记

Java并发编程实战-14高级主题_原子变量与非阻塞同步机制
Publish: 2018/7/23   

锁的劣势

硬件对并发的支持

比较并交换(CAS)

    // 模拟CAS操作
    @ThreadSafe
    public class SimulatedCAS{
        @GuardedBy("this") private int value;

        public synchronized int get() { return value; }

        public synchronized int compareAndSwap(int expectedValue, int newValue){
            int oldValue = value;
            if(oldValue == expectedValue)
                value = newValue;
            return oldValue;
        }

        public synchronized boolean compareAndSet(int expectedValue,int newValue){
            return (expectedValue==compareAndSwap(expectedValue,newValue));
        }
    }

CAS包含了3个操作数——需要读写的内存位置V、进行比较的值A和拟写入的新值B。当且仅当V的值等于A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作。无论位置V的值释放等于A,都将返回V原有的值。

当多个线程尝试使用CAS同时更新一个变量时,只有其中一个线程能更新变量的值,而其他线程都将失败。然而,失败的线程并不会被挂起,而是被告知在这次竞争中失败,并可以再次尝试,或者执行一些恢复操作,或者不执行任何操作。

CAS的典型使用模式是:首先从V中读取值A,并根据A计算新值B,然后再通过CAS以原子方式将V中的值由A变成B(只要在这期间没有任何线程将V的值修改为其他值)。

非阻塞的计数器

    //基于CAS实现的非阻塞计数器
    @ThreadSafe
    public class CasCounter{
        private SimulatedCAS value;

        public int getValue(){
            return value.get();
        }

        public int increment(){
            int v;
            do{
                v=value.get();
            }
            while(v!=value.compareAndSwap(v,v+1));
            return v+1;
        }
    }    

当竞争程度不高时,基于CAS的计数器在性能上远远超过了基于锁的计数器,而在没有竞争时甚至更高。

Java语言在实现锁定时需要遍历JVM中一条非常复杂的代码路径,并可能导致操作系统的锁的、线程挂起以及上下文切换等操作,在最好的情况下,至少需要一次CAS。在程序内部执行CAS时不需要执行JVM代码、系统调用货线程调度操作。

CAS的主要缺点是他将使调用者处理竞争问题(通过重试、回退、放弃),而在锁中能自动处理竞争问题(线程在获得锁之前将椅子阻塞)。

原子变量类

原子变量类比锁的粒度更细,量级更轻,将发生竞争的范围缩小到单个变量上。更新原子变量的快速(非竞争)路径不会比获取锁的快速路径慢,并且通常会更快,而它的慢速路径肯定比锁的慢速路径快,因为它不需要挂起或重新调度线程。在使用基于原子变量而非锁的算法中,线程在执行时更不易出现延迟,并且如果遇到竞争,也更容易恢复过来。

原子变量类能够支持原子的和有条件的读—改—写操作。AtomicInteger表示一个int类型的值,并提供了get和set方法,这些Volatile类型的int变量在读取和写入上有着相同的内存语义。它还提供了一个原子的compareAndSet方法(如果该方法成功执行,那么将实现与读取/写入一个volatile变量相同的内存效果),以及原子的添加、递增和递减等方法。

共有12个原子变量类,分四组:(都支持CAS)

原子变量是一种”更好的volatile”

使用AtomicReference和IntPair来保存状态,并通过使用compareAndSet,使它在更新上界或下界时能避免NumberRange的竞态条件。

    //通过CAS来维持包含多个变量的不变性条件
    public class CasNumberRange{
        @Immutable
        private static class IntPair{
            final int lower;//不变性条件:lower<=upper final="" int="" upper;="" ...="" }="" private="" atomicreference values = 
            new AtomicReference(new IntPair(0,0));

        public int getLower(){return values.get().lower;}
        public int getUpper(){return values.get().upper;}

        public void setLower(int i){
            while(true){
                IntPair oldv=values.get();
                if(i>oldv.upper)
                    throw new IllegalArgumentException(
                        "Can't set lower to "+ i +" >upper");
                IntPair newv=new IntPair(i,oldv.upper);
                if(values.compareAndSet(oldv,newv))
                    return;
            }
        }
        //对setUpper采用类似的方法
    }

性能比较:锁与原子变量

在高度竞争的情况下,锁的性能将超过原子变量的性能,但在更真实的竞争情况下,原子变量的性能将超过锁的性能。

非阻塞算法

如果在某种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法。如果在算法的每个步骤中都存在某个线程能够执行下去,那么这种算法被称为无锁算法。如果在算法中仅将CAS用于协调线程之间的操作,并且能正确的实现,那么它即使一种无阻塞算法又是一种无锁算法。

非阻塞的栈

栈是最低暗淡的链式数据结构:每个元素仅指向一个元素,并且每个元素也只被一个元素引用。

非阻塞算法的特性:某项工作的完成具有不确定性,必须重新执行。

创建一个新的节点,该节点的next域指向当前的栈顶,然后使用CAS把这个新节点放入栈顶。如果在开始插入节点时,位于栈顶的节点没有变化,那么CAS就会成功,如果栈顶节点发生了变化,那么CAS将会失败,而push方法会根据栈的当前状态来更新节点,并且再次尝试。无论哪种情况,在CAS执行完成后,后栈仍会处于一致的状态。

    //使用Treiber算法构造的非阻塞栈
    @ThreadSafe
    public class ConcurrentStack < E >{
        AtomicReference < E > top = new AtomicReference < Node < > >();

        public void push(E item){
            Node < E > newHead = new Node < E >(item);
            Node < E > oldHead;
            do{
                oldHead=top.get();
                newHead.next=oldHead;
            }while(!top.compareAndSet(oldHead,newHead));
        }

        public E pop(){
            Node< E > oldHead;
            Node< E > newHead;
            do{
                oldHead = top.get();
                if(oldHead==null)
                    return null;
                newHead=oldHead.next;
            }whild(!top.compareAndSet(oldHead,newHead));
            return oldHead.item;
        }

        private static class Node < E >{
            public final E item;
            public Node<> next;

            public Node(E item){
                this.item=item;
            }
        }
    }

非阻塞的链表

    //Michael-Scott非阻塞算法中的插入算法
    @ThreadSafe
    public class LinkedQueue{
        preivate static class Node{
            final E item;
            final AtomicReference>

            public Node(E item,Node next){
                this.item=item;
                this.next=new AtomicReference>(next);
            }

            private final Node dummy = new Node(null,null);
            private final AtomicReference> head 
                = new AtomicReference>(dummy);

            public boolean put(E item){
                Node newNode=new Node(item,null);
                Node tailNext=curTail.next.get();
                if(curTail==tail.get()){
                    if(tailNext!=null){
                        //队列处于中间状态,推进尾节点
                        tail.compareAndSet(curTail,tailNext);
                    }else{
                        //处于稳定状态,尝试插入新节点
                        if(curTail.next.compareAndSet(null,newNode)){
                            //插入操作成功,尝试推进尾节点
                            tail.compareAndSet(curTail,newNode);
                            return true;
                        }
                    }
                }
            }
        }
    }

原子的域更新器

    //在ConcurrentLinkedQueue中使用原子的域更新器
    private class Node{
        private final E item;
        private volatile Node next;

        public Node(E item){
            this.item=item;
        }
    }

    private static AtomicReferenceFieldUpdater nextUpdater
        =AtomicReferenceFieldUpdater.newUpdater(Node.class,Node.class,"next");

ABA问题

如果在算法中的节点可以被循环使用,那么在使用”比较并交换”指令时就可能出现这种问题(主要在没有垃圾回收机制的环境中)。在CAS操作中将判断V的值是否仍然为A,并且如果是的话就继续指向更新操作。

AtomicStampedReference(以及AtomicMarkableReference)支持在两个变量上执行原子的条件更新。AtomicStampedReference将更新一个”对象——引用”二元组,通过在引用上加上”版本号”,从而避免ABA问题。类似地,AtomicMarkableReference将更新也给”对象引用——布尔值”二元组,在某些算法中将通过这种二元组使节点保存在链表中同时又将其标记为”已删除的节点”。



← Java并发编程实战-15高级主题_Java内存模型 Java并发编程实战-12高级主题_显示锁 →

Powered by Hexo, Theme designs by @hpcslag.
Style-Framework Tocas-UI designs by @yamioldmel