Java中的乐观锁

1、前言

之前好几次看到有人在面经中提到了乐观锁与悲观锁,但是一本《Java Concurrency In Practice》快看完了都没有见到过这两种锁,今天终于在第15章发现了它们的踪迹。

15.2 Hardware support for concurrency

Exclusive locking is a pessimistic technique—it assumes the worst (if you don’t lock your door, gremlins will come in and rearrange your stuff) and doesn’t proceed until you can guarantee, by acquiring the appropriate locks, that other threads will not interfere.

For fine-grained operations, there is an alternate approach that is often more efficient—the optimistic approach, whereby you proceed with an update, hopeful that you can complete it without interference. This approach relies on collision detection to determine if there has been interference from other parties during the update, in which case the operation fails and can be retried (or not). The optimistic approach is like the old saying, “It is easier to obtain forgiveness than permission”, where “easier” here means “more efficient”.

原来乐观锁与悲观锁并不是特指某个锁,而是在并发情况下保证数据完整性的不同策略。悲观锁指的就是我们平常使用的加锁机制,它假设我们总是处于最坏的情况下,如果不加锁数据完整性就会被破坏。而乐观锁指是一种基于冲突检测的方法,检测到冲突时操作就会失败。

2、CAS机制介绍

CAS(Compare And Swap)是一种常见的“乐观锁”,大部分的CPU都有对应的汇编指令,它有三个操作数:内存地址V,旧值A,新值B。只有当前内存地址V上的值是A,B才会被写到V上,否则操作失败。

public class SimulatedCAS {
  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;
  }
}

上边的类模拟了CAS操作,如果成员变量 value 的值与参数 expecredValue 的值不同,那就说明其他的线程已对其进行了修改,本次操作失败。

接下来看一个使用CAS实现线程安全的计数器的例子。

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的乐观锁在性能上要优于使用加锁方式的悲观锁,因为大部分情况下经过数次轮询后CAS操作都可以成功,而使用加锁机制则会造成线程的阻塞与调度,相对而言更耗时。

Java从5.0开始引入了对CAS的支持,与之对应的是 java.util.concurrent.atomic 包下的AtomicInteger、AtomicReference等类,它们提供了基于CAS的读写操作和并发环境下的内存可见性。

3、非阻塞算法

通过CAS可以实现高效的支持并发访问的数据结构,首先来看一个栈的实现。

public class ConcurrentStack <E> {
    AtomicReference<Node<E>> top = new AtomicReference<>();
    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;
        } while (!top.compareAndSet(oldHead, newHead));
        return oldHead.item;
    }
}

public class Node <E> {
    public final E item;
    public Node<E> next;
    public Node(E item) {
        this.item = item;
    }
}

这个栈实际上是一个单链表结构,变量 top 代表头节点。当 push() 被调用后,首先新建一个节点 newHead ,它的后继节点应该为当前的头节点。然后使用CAS操作尝试将新节点赋值给 top ,如果 top 没有发生变化,CAS操作成功。如果 top 已改变(可能是某个线程加入或移除了元素),CAS操作失败,替换新节点的后继节点为当前的头节点,再次尝试。无论CAS操作是否成功,链表的结构都不会被破坏。

现在为止我们已经看了两个基于CAS操作的例子,一个是计数器另一个是栈,它们都有一个共同的特点,那就是它们的状态只由一个变量决定,而且显然CAS操作一个也只能更新一个变量。那么如何使用CAS实现更为复杂的数据结构呢?这里给出一个队列的例子。

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

public class LinkedQueue <E> {
    private final Node<E> dummy = new Node<E>(null, null);
    private final AtomicReference<Node<E>> head = new AtomicReference<Node<E>>(dummy);
    private final AtomicReference<Node<E>> tail = new AtomicReference<Node<E>>(dummy);
    public boolean put(E item) {
        Node<E> newNode = new Node<E>(item, null);
        while (true) {
            Node<E> curTail = tail.get();
            Node<E> tailNext = curTail.next.get();
            if (curTail == tail.get()) {
                if (tailNext != null) {
                    // Queue in intermediate state, advance tail
                    tail.compareAndSet(curTail, tailNext);
                } else {
                    // In quiescent state, try inserting new node
                    if (curTail.next.compareAndSet(null, newNode)) {
                        // Insertion succeeded, try advancing tail
                        tail.compareAndSet(curTail, newNode);
                        return true;
                    }
                }
            }
        }
    }
}

为了维护队列结构的有效性,我们必须要做到,如果线程A正在修改链表,线程B即将修改链表,线程B应该可以得知当前某个线程正对链表进行操作,线程B不可以立即对其进行修改。然后,如果线程B发现线程A正在修改链表,链表中应该含有足够的信息使线程B能够“帮助”线程A完成工作。线程B完成了线程A未完成的工作后,线程B就可以立即开始执行自己的任务,而且线程A应该可以知道,线程B已经替自己完成了剩下的工作。这个算法实际是Michael-Scott nonblocking linked-queue algorithm,JDK中的ConcurrentLinkedQueue就是使用了这个算法。

现在来看一下具体的实现,首先表头 head 跟表尾 tail 在初始化时被指向了一个名叫 dummy 的哨兵节点。如果链表正在被修改,指针状态如下:

修改完成后指针状态如下:

 

算法的关键之处就在于,如果链表正在被修改,那么 tail 指向的节点的 next 属性是不为 null 的。那么任何线程只要发现 tail.next 不为 null ,它就可以断定链表当前正在被另一个线程操作。而且更巧妙的是,这个线程可以通过将 tail.next 赋值给 tail 来帮助另一个线程完成工作。

附:

在上边的队列中,每个Node节点在初始化时都要新创建一个AtomicReference对象,我们还可以对其进行优化:

public class Node<E> {
    private final E item;
    private volatile Node<E> next;
    public Node(E item) {
        this.item = item;
    }
}

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

通过AtomicReferenceFieldUpdater,我们可以用CAS的方式对 volatile 修饰变量进行修改,避免创建额外的对象。

 

参考:《Java Concurrency In Practice》

posted @ 2018-03-22 18:49  mmmmar  阅读(21844)  评论(0编辑  收藏  举报