由ReentrantLock的非公平锁简画流程和源码分析了解AQS

AQS:AbstractQueuedSynchronizer, 队列同步器,它是Java并发用来构建锁和其他同步组件的基础框架
官方文档:提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量,事件等)。 该类被设计为大多数类型的同步器的有用依据,这些同步器依赖于单个原子int值来表示状态。主要是通过维护一个state 变量以及用来存放阻塞线程的双向队列来控制同步

state 0代表释放,1或者>1(锁重入)代表被占有,

从ReentrantLock的非公平锁的lock实现来看AQS:

一开始没想写源码分析,总感觉少点什么,补上之后,结合流程图和AQS队列流程还是比较容易理解的,请耐心看源码分析的注释

 

由ReentrantLock的非公平锁简画流程和源码分析了解AQS_第1张图片

对着源码看更容易理解,难于理解的在于对AQS双向队列的操作:

AQS双向队列操作:假设ABC三个线程,A先获取到锁

由ReentrantLock的非公平锁简画流程和源码分析了解AQS_第2张图片

锁释放:

由ReentrantLock的非公平锁简画流程和源码分析了解AQS_第3张图片

ReentrantLock源码分析:

  //非公平锁NonfairSync
  static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        //lock入口
        final void lock() {
  
             //CAS 直接去获取锁,尝试修改state 0变为1。修改成功则获取锁成功。
             //这里用CAS 修改成功返回true,修改失败说明state已经不是0,被其他线程抢先修改了,返           
             //回false
            if (compareAndSetState(0, 1))
                //成功获取锁,设置持有锁的线程为自己也就是当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //CAS获取所失败,再次尝试获取锁,如果锁已经释放,这步就会有很大的性能提高
                //这里是父类的抽象,子类实现就是下面的tryAcquire
                acquire(1);
        }
        //非公平锁尝试获取锁
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

下面看CAS获取锁失败的逻辑,进入acauire(1): 

//AQS尝试获取锁
 public final void acquire(int arg) {
           //tryAcquire需看子类的具体实现,这里看的是NonfairSync的tryAcquire
           //实现nonfairTryAcquire
        if (!tryAcquire(arg) &&
             //尝试获取所失败,会为当前线程建立线程节点放入队列尾走addWaiter(Node.EXCLUSIVE)
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            
           //中断补偿
            selfInterrupt();
    }
 //NonfairSync的tryAcquire实现
 //尝试获取锁,true获取锁成功,false失败
 final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //state=0可以进行获取锁
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    //CAS成功获取锁,设置持有锁的线程为当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //c!=0说明锁已经被持有,判断是已经持有锁的线程,(可重入)
            else if (current == getExclusiveOwnerThread()) {
                //可重入锁 state+1
                int nextc = c + acquires;
                //加1之后小于0,state值不对抛出异常
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                //可重入锁加1后赋值
                setState(nextc);
                return true;
            }
            return false;
        }


  //新建线程节点,mode以给定模式构造结点,有两种:EXCLUSIVE(独占)和SHARED(共享)这里是独占
  //ReentrantLock为独占锁
  private Node addWaiter(Node mode) {
        //为当前线程建立节点,独占模式,当前线程也就是刚刚来尝试获取锁失败的线程
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        //这里想把新建的节点放入队尾,所以当前队尾的next 应指向新建的节点
        Node pred = tail;
        //当前队尾不为空,就是说当前队列里有没有其他线程节点在等待
        if (pred != null) {
            //新建节点的前置指向当前队尾节点,
            //这里就像流程图里,放入C线程节点的时候,C的前置指向B
            node.prev = pred;
            //然后CAS更新尾巴节点为新建节点
            if (compareAndSetTail(pred, node)) {
                //更新成功,把队列的当前队尾节点的下一个节点指向新建节点
                //因为是双向队列,要改变或确定一个节点的位置 就要指定节点的prev和next
                //就是我在你后面,我的前面是你,你的候面是我,才能确定我在你后面
                //这里有个问题,因为分三步操作:1 修改新建节点的前置。2 CAS修改尾巴节点tail
                //3 修改新建节点的前置节点的后置,三步操作不是原子操作,思考点,有可能我
                //的前置节点修改好了,但是我的前置节点的后置却是null--就是我知道我前面是你,
                //你不知道你后面是我                        
                pred.next = node;
                //返回新建节点
                return node;
            }
        }
        //这里如果尾巴节点为null,说明目前还没有线程节点在队列里,需初始化头节点,并把新建节点
        //放在头节点的候面
        //这里可以看流程图的,B线程节点入队的时候,
        enq(node);
        return node;
    }

  //初始化头节点,并把新建的线程节点放入头节点候面。可看流程图B节点入队的时候
  private Node enq(final Node node) {
        //这个是死循环直到新建节点放入队列尾成功,跳出循环
        for (;;) {
            //再次判断尾巴节点是不是空,因为可能中途被其他线程新建了
            Node t = tail;
            if (t == null) { // Must initialize
                 //CAS创建头节点
                if (compareAndSetHead(new Node()))
                    //初始化头节点也是尾巴节点
                    tail = head;
            }
            //头节点被初始化之后,下次循环走这个
            else {
                //这块和头节点不为空的操作是一样的,新建节点的前置指向尾巴节点
                node.prev = t;
                //CAS 更新尾巴节点为新建节点
                if (compareAndSetTail(t, node)) {
                    //原来的尾巴节点的后置指向新建节点
                    t.next = node;
                    //返回新建节点
                    return t;
                }
            }
        }
    }

新建节点放入队尾之后,看接下来的操作,分析acquireQueued方法,这个方法是我认为不太好理解的地方,包括线程自旋获取锁,

获取失败之后标记前置节点的waitStatus为SIGNAL,然后自身park 停止自旋,前置节点释放掉锁之后,unpark唤醒后置最近的有效节点(实际处理起来并不像说的那么简单),看Doug Lea大师怎么处理的:

//新入队列的节点都会走这一步,从流程图线程B节点入队来分析 
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //死循环也就是自旋,这里每个节点至少自旋一次去获取锁,获取失败后线程走else,会让线程            
            //park,park后就不再自旋了,等待unpark唤醒
            for (;;) {
                //获取新建节点的前置
                final Node p = node.predecessor();
                //如果新建节点的前置为头节点,那么新建节点会再次尝试获取锁
                //如果此时锁被释放,成功获取锁,
                if (p == head && tryAcquire(arg)) {
                   //设置新的头节点为新建节点,
                   //流程图的B线程节点成功获取锁,头节点被放弃,
                   //新的头节点为B
                    setHead(node);
                    //让原来的头节点指向为null ,GC会回收掉。头节点prev本身就是null
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //1.如果说B线程节点获取锁失败
                //2.或者说新建节点的前置不是头节点,可看流程图线程C线程节点入队,他的前置
                //节点是B节点
                //如果获得锁失败,根据waitStatus决定是否park线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    //前面执行为true会执行这一步park线程,等待unpark唤醒后,
                    //检查线程节点线程是否被中断,被中断则返回true
                    //如果被中断过,阻塞中的线程并不会响应中断,回到acquire()看到
                    //selfInterrupt() 补偿中断
                    parkAndCheckInterrupt())
                                             
                    interrupted = true;
            }
        } finally {
            //抛出异常,取消节点操作,出队操作,这块没啥好说的
            if (failed)
                cancelAcquire(node);
        }
    }

 //主要把当前节点的前置节点waitStatus标记为SIGNAL,释放锁后好唤醒当前节点
 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        //如果为SIGNAL也就是-1 说明已经被标记过,直接返回
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        //waitStatus>0说明前置节点是已经撤销无效的节点,跳过
        //拿流程图的BC来说明,BC都在等待,然后突然B不想等待了,撤销了,那么由谁来唤醒C,
        //C就往前继续找有效的前置节点,找到了头节点
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } 
        //这里如果waitStatus没有被修改过,waitStatus默认为0
        //或者说小于0又不是SIGNAL,也就只有PROPAGATE
        //PROPAGATE在共享锁以及condition队列里使用,这里不做介绍
        else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            //CAS 更改前置节点的waitStatus为SIGNAL,这样前置节点释放锁后会唤醒当前节点node
            //这里就会发现,从AQS队列操作流程图来看,B节点把A节点的waitStatus置为SIGNAL,
            //C节点把B节点的waitStatus置为SIGNAL,这样A节点释放锁,会唤醒B,B执行完释放锁会
            //唤醒C
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }


加锁到这里就结束了。下面看锁的释放,锁释放比较简单:

 //释放锁
 public final boolean release(int arg) {
        if (tryRelease(arg)) {
          
            Node h = head;
            //成功释放锁后,判断队列中是否有需要唤醒的线程节点
            if (h != null && h.waitStatus != 0)
                //唤醒下一个有效节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

 //子类实现,尝试释放锁
 protected final boolean tryRelease(int releases) {
            //
            int c = getState() - releases;
            //释放锁的线程肯定需是持有锁的线程
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //锁释放完毕
            if (c == 0) {
                free = true;
                //持锁者为空
                setExclusiveOwnerThread(null);
            }
            //更新state ,这里如果state =0释放锁成功,如果不等于0,
            //还有资源没释放(可重入锁机制)
            setState(c);
            return free;
        }

   //唤醒节点
   private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        
        if (ws < 0)
            //这里的ws 一般是-1
            //修改当前节点的waitStatus为0就是我可以唤醒别人了
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        //当前节点后置没有节点或者节点无效
        if (s == null || s.waitStatus > 0) {
            s = null;
            //从队列尾部往前遍历寻找当前节点最近的有效节点
            //这里为什么从后往前遍历,因为在加锁的时候,新建节点的时候那个思考?
            //回到addWaiter看,三步不是原子操作。有可能头节点的next节点为空。但是其实已经存在了
            //只是还没有做pred.next = node这一步,所以从后往前遍历一定会遍历到所以节点                   
            for (Node t = tail; t != null && t != node; t = t.prev)
                //找到唤醒的节点t
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            //唤醒s线程继续可以自旋争夺锁
            LockSupport.unpark(s.thread);
    }

到这里就结束了:分析过程中遇到几个问题。查了很多:park 使当前线程取消自旋(即使在死循环里),一开始对为什么释放锁的时候寻找后置节点从后遍历有疑问,因为感觉从前遍历一下不就找到了吗,分析完之后发现了原因。

这里再说下简述下公平锁吧,公平锁和非公平锁不同就是,公平锁不会直接CAS获取锁,而是先去队列里判断有没有等待的线程,如果有等待的线程,那么直接排在候面,如果只有头节点或者没有等待的线程他会尝试获取锁,主要看hasQueuedPredecessors这个方法,其他的都和公平锁一样了

AQS里面使用了大量的CAS和unsafe类的方法,有兴趣可以了解一下,底层C++实现的

AQS的静态内部类Node 也可以具体了解下

AQS里还有很多没分析到,有在像Semaphore,CountDownLatch里面用到的,后续再去学习,关于AQS 网上太多文章了,我这里就当作自己的一次学习笔记吧

你可能感兴趣的:(java,并发编程)