《Java并发编程实践》笔记7——非阻塞同步算法

1.锁的劣势:
锁是实现线程同步最简单的方式,也是代价最高的方式,其有如下的缺点:
(1).重量级:
现代JVM对非竞争的锁的获取和释放进行优化,可以根据系统中锁占用的历史数据决定使用自旋还是挂起等待,使得它非常高效。但是如果有多个线程同时请求锁,JVM就需要向操作系统寻求帮助,没有获取到锁的线程可能会被挂起等待,并稍后恢复运行。线程的挂起和恢复会带来很大的上下文切换和调度延时开销。


(2).优先级倒置:
当一个线程在等待锁时,它不能做任何其他事情,若一个线程在持有锁的情况下发生了延迟(页面错误、调度延迟等),那么其他需要该锁的线程都不能前进了,若被阻塞的线程是高优先级线程,持有锁的线程优先级较低,那么高优先级的线程仍然需要等待低优先级线程释放锁,导致优先级降低,这就是优先级倒置问题。


(3).悲观性:
锁默认是独占锁(读-写锁的读锁除外),它是一种悲观锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
2.Volatile的局限性:
与锁相比,volatile变量是一种更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换和线程调度等操作,但是volatile变量也存在一些局限:volatile只能保证变量对各个线程的可见性,不能用于构建原子的复合操作,即不能保证原子性。因此当一个变量依赖旧值时就不能使用volatile变量。


3.非阻塞同步算法的优势:
(1).乐观性:
非阻塞同步算法是一种乐观锁,它基于冲突检测,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
(2).硬件支持:
非阻塞同步算法底层使用原子化的机器指令(比如比较并交换,compare-and-set)取代锁,从而保证数据在并发访问下的一致性。


在大多数处理器架构,包括IA32、Space中采用的都是CAS指令(PowerPc使用的加载链接/存储条件指令,load-linked/store-conditional),CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当V符合旧预期值A时,CAS用新值B原子化地更新V的值,否则什么都不做。CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。


以synchronized和int模拟CAS算法,实现类似AtomicInteger的原子自增算法,例子代码如下:

public class SimulatedCAS {
    private int value;

    public synchronized int get() {
        return value;
    }

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

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

public class CasCounter {
    private SimulatedCAS value;

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

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

(3).非阻塞且锁自由:
非阻塞是指一个线程的失败或挂起不应该影响其他行程的失败或挂起;锁自由是指算法的每一步骤中都有一些线程能够继续执行。

基于CAS的非阻塞同步算法即是非阻塞又是锁自由的,非竞争的CAS总能成功,如果多个线程竞争一个CAS,总会有一个线程胜出并前进,因此非阻塞算法对于死锁和优先级倒置问题具有免疫性(可能会有饥饿或活锁问题,因为允许重试)。


4.非阻塞算法与锁的选择:
非阻塞算法与锁相比,非阻塞算法的设计和实现复杂度高,可伸缩性和活跃度高,同时对死锁和优先级倒置问题具有免疫性(可能会有饥饿或活锁问题)。
类似于数据库的乐观锁和悲观锁,大部分情况下非阻塞算法的性能高于锁,但是在激烈竞争条件下锁性能胜过非阻塞算法。


5.JDK中对CAS非阻塞算法的支持:
JDK1.5引入的原子类变量中,如java.util.concurrent.atomic中的AtomicXxx,都使用了底层JVM支持的非阻塞原子化的机器指令,为数字类型的引用类型提供一种高效的CAS操作(compareAndSet方法),而在java.util.concurrent中的大多数类在实现时都直接或间接的使用了这些原子变量类。
使用原子引用AtomicReference的CAS算法实现一个非阻塞栈,例子代码如下:

public class ConcurrentStack<E> {
    AtomicReference<Node<E>> top = new AtomicReference<Node<E>>();

    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> newHead;
        Node<E> oldHead;
        do {
            oldHead = top.get();
            if (oldHead == null) {
                return null;
            }
            newHead = oldHead.next;
        } while (!top.compareAndSet(oldHead, newHead));
        return oldHead.item;
    }

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

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

6.CAS中的ABA问题:
一般的CAS在决定是否要修改某个变量时,会判断一下当前值跟旧值是否相等。如果相等,则认为变量未被其他线程修改,可以改。
但是,“相等”并不真的意味着“未被修改”。 另一个线程可能会把变量的值从A改成B,又从B改回成A,这就是ABA问题。如果在算法中的节点可以被循环使用,那么在使用“比较并交换”指令时就可能出现ABA问题(如果在没有垃圾回收机制的环境中)。


很多情况下,ABA问题不会影响你的业务逻辑因此可以忽略。但有时不能忽略,这时要解决这个问题,一般的做法是给变量关联一个只能递增、不能递减的版本号。在比较时不但比较变量值,还要再比较一下版本号,JDK的AtomicStampedReference和AtomicMarkableReference类就是使用版本号来解决ABA问题的。

相关推荐