深入理解AQS-2 锁基础知识

悲观锁和乐观锁

synchronized同步方法最主要的问题是线程阻塞和唤醒带来的性能消耗,阻塞同步是悲观的并发策略,只要有可能出现竞争,都认为一定要先加锁;然而还有一种乐观的并发策略,直接操作数据,如果没有发现其他线程同时操作数据则认为这个操作是成功的,如果其他线程也操作了数据,那么操作是失败的,一般采用不断重试的手段(自旋),直到成功为止。乐观策略适用于并发程度不高且临界区较小的场景,优点是不需要阻塞线程,属于非阻塞同步手段,性能更高。

CAS

乐观锁并发策略主要有两个重点阶段,一个是对数据进行操作,另一个是检测是否发生冲突(即是否存在同时操作数据的其他线程),这里操作数据和冲突检测需要具备原子性,即操作数据和冲突检查必须同时成功或者同时失败,这个原子性通过CAS指令来实现,目前绝大多数的CPU都支持CAS指令。

CAS指令需要三个参数:分别是内存地址,期望的旧值,新值。

自旋锁

自旋锁是乐观锁的一种实现,当线程曲获取一个锁时,如果发现该锁被其他线程占用,那么进入一个无意义的循环不断尝试加锁,直到成功获取锁。自旋锁适用于临界区比较小的场景,如果锁被持有的时间过长,那么自旋本身会长时间的白白浪费CPU资源。

public class SpinLock {
    private final AtomicReference<Thread> owner = new AtomicReference<Thread>();
    public  void lock(){
        while (!owner.compareAndSet(null, Thread.currentThread())){
            System.out.println(Thread.currentThread().getName() + " loop");
        }
        System.out.println(Thread.currentThread().getName() + " lock");
    }
    public void unlock(){
        owner.compareAndSet(Thread.currentThread(), null);
        System.out.println(Thread.currentThread().getName() + " unlcok");
    }
}

上述代码中owner变量保存了持锁线程,这里有两个缺点,第一个是没有保证公平性,另一个是由于多个线程同时操作同一个共享变量owner,而每个CPU都会缓存该变量,任意一个线程加锁和解锁后,其他所有CPU中的owner缓存都立刻失效,而线程自旋过程中又都会使用(读)该变量,所以各个CPU都需要重新读内存(CPU的缓存一致性原理),因此自旋锁会频繁的进行缓存一致性同步操作,每次加锁和解锁都会带来一次,这导致繁重的系统总线流量和内存操作,会降低性能。

公平自旋锁

为了解决公平性问题,可以让锁维护一个编号来表示下次该获取锁的线程,每个线程在申请锁时首先被分配一个编号,然后始终自旋直到轮到自己然后尝试加锁。虽然解决了公平性问题,但是依然存在缓存同步导致性能下降的问题。

public class FairSpinLock {
    private final AtomicInteger nextNo = new AtomicInteger();
    private final AtomicInteger threadNo = new AtomicInteger();
    public  int lock(){
        int myNo = threadNo.getAndIncrement();
        while (nextNo.get() != myNo){
        }
        System.out.println(Thread.currentThread().getName() + " lock");
        return myNo;
    }
    public void unlock(int threadNo){
        int next = threadNo + 1;
        nextNo.compareAndSet(threadNo, next);
        System.out.println(Thread.currentThread().getName() + " unlcok");
    }
}

MCS自旋锁

自旋锁之所以频繁的发生缓存失效的问题,是因为所有线程加锁和解锁都会操作同一个变量,因此如果是不同的变量,或者说多个线程操作不同变量,那么可避免高频率的缓存同步操作,这就是MCS的实现思路。

MCS基于链表实现,每个申请锁的线程都对应链表上的一个节点,这些线程一直轮询自身节点来确定自己是否获得了锁。获得锁的线程在释放锁的时候,负责通知后继节点已获取锁,即更新后继节点的运行状态,这会导致其他CPU中该变量的缓存失效,但并不是所有线程都会使用这个后继节点,所以不会发生所有CPU同时进行缓存一致性同步操作,而且仅在线程通知后继线程时发生一次缓存失效,这样缓存同步操作就减少很多,降低了系统总线和内存的开销。

不支持重入,可加一个ThreadLocal变量记录重入次数来实现。

public class McsLock {
    /**
     *   队尾
     */
    private volatile Node tail;

    /**
     * 队尾原子操作
     */
    private static final AtomicReferenceFieldUpdater<McsLock, Node> TAIL_UPDATER =
            AtomicReferenceFieldUpdater.newUpdater(McsLock.class, Node.class, "tail");
    /**
     * 线程到节点的映射
     */
    private ThreadLocal<Node>  currentThreadNode = new ThreadLocal<>();

    /**
     * 加锁
     */
    public void lock(){
        Node cNode = currentThreadNode.get();
        if (cNode == null){
            cNode =  new Node();
            currentThreadNode.set(cNode);
        }
        //原子地入队并返回原队尾
        Node predecessor = TAIL_UPDATER.getAndSet(this, cNode);  //step 1
        if (predecessor != null){
            //需要排队
            predecessor.setNext(cNode);   //step 2
            while (cNode.isWaiting){
                //自旋等待前置线程更新自己的状态   //step
            }
        }else{
            //无需排队,自己更新自己状态
            cNode.setWaiting(false);
        }
        System.out.println(Thread.currentThread().getName() + " lock");
        return;
    }

    /**
     * 解锁
     */
    public void unlock(){
        // 获取当前线程对应的节点
        Node cNode = currentThreadNode.get();
        if (cNode == null || cNode.isWaiting){
            throw new RuntimeException(cNode + " not lock");
        }
        if (cNode.getNext() == null && !TAIL_UPDATER.compareAndSet(this, cNode, null)){
            // 没有后继节点的情况,将tail置空
            // 如果CAS操作失败了表示突然有节点排在自己后面了,可能还不知道是谁,下面是等待后继节点入队
            // 这里之所以要忙等是因为上述的lock操作中step 1执行完后,step 2可能还没执行完
            while (cNode.getNext() == null){
                //step 5
            }
        }
        if (cNode.getNext() != null){
            // 通知后继节点获取锁
            cNode.getNext().setWaiting(false);
            //help GC
            cNode.setNext(null);
        }
        System.out.println(Thread.currentThread().getName() + " unlock");
    }

    /**
     * 队列节点类
     */
    private static class Node {
        //默认是等待状态
        private volatile boolean isWaiting = true;
        //后继节点
        private volatile Node next;

        public boolean isWaiting() {
            return isWaiting;
        }

        public void setWaiting(boolean waiting) {
            isWaiting = waiting;
        }

        public Node getNext() {
            return next;
        }

        public void setNext(Node next) {
            this.next = next;
        }
    }

CLH自旋锁

CLH锁和MCS锁的原理大致相同,都是各个线程各自关注各自的变量,来避免多线程同时操作同一个变量,从而减少缓存同步;不同点在于MCS自旋轮询的是当前节点的属性,而CLH轮询的是前驱节点的属性,来判断前一个线程是否释放了锁。

public class ClhLock {
    public static class Node {
        //是否结束
        private volatile boolean isOver = false;
    }
    private volatile Node tail;

    private static AtomicReferenceFieldUpdater<ClhLock, Node> TAIL_UPDATER =
            AtomicReferenceFieldUpdater.newUpdater(ClhLock.class, Node.class, "tail");

    public void lock(Node cThread){
        Node predesser = TAIL_UPDATER.getAndSet(this, cThread);
        if (null != predesser){
            while (predesser.isOver){}
        }
        System.out.println(Thread.currentThread().getName() + " lock");
    }

    public void unlock(Node cThread){
        if (!TAIL_UPDATER.compareAndSet(this, cThread, null)){
            //存在等待线程
            cThread.isOver = true;
        }
        System.out.println(Thread.currentThread().getName() + " unlock");
    }

从代码可知,CLH比MCS要简洁很多;CLH是在前驱节点的属性上自旋,其等待队列是隐式的,节点并不实际持有前驱或者后继,通过每个节点都轮询前驱形成逻辑链表,而MCS的队列是物理存在的

你可能感兴趣的:(锁,java)