aqs 原理

aqs 原理

1.介绍aqs

1.1 扯淡

我的理解:

一个管理状态值和队列同步器.

状态值:是否可以被占有.(Node的状态,比如被占有啊等待啊,初始啊...aqs提供api,子类来重写)

队列:等待获取锁的队列(FIFO) .

同步器: 一次只活跃一个Node(或者多个node,比如独占锁和共享锁).

发生竞争的多线程,被抽象成一个队列,按照aqs的规则,合理执行.
//关于依赖状态值的并发代码

那么依赖状态值,来控制代码,有如下的思考过程.
    
==>一个这样的demo.生产/消费,如果满了,就不生产,如果没了,就生产

==>那么当程序遇到false时候,可以通过轮询或者睡眠合理的时间,等待条件为true,这样还是会有空档期,执行效率不高
==>这些都是,等待唤醒

==>触发唤醒
==>在到条件队列(wait,notify),需要注意notify,notifyAll.以及信号丢失(过早wait或者其他),就那块的小坑.
这种信号驱动的唤醒机制,会比轮询睡眠那种快很多,可是有这个问题,notifyAll,相当于所有等待线程被唤醒,在重新竞争一次,这样还是会浪费计算机资源(竞争,和上下文操作)    

==>其实上面的问题,就是一个锁的问题.

--------------------------------------------------------------------------------------    
==>ReentrantLock,利用的是信号驱动(park,unpark),替换这个wait,notify.
    
==>在到后来我们可以想办法把,锁这个东西封装,信号驱动的事情封装,在有了reentrantlock中的condition
通过维护相应的队列,合适的状态,并且封装好.

结论:利用条件队列,信号驱动,解决好计算机竞争,以及其他问题.而aqs,在使用条件队列维护状态值,的时候,就特别巧妙.就是说,如何设计这个条件队列,让他吞吐量最高,或者功能更好?    fifo 队列,信号驱动(park,unpark)

    
--------------------------------------------------------------------------------------   
  synchronize, 和reentrantLock.先分开看,分别学他们的原理.再来体会区别  
    
//关于锁  synchronize 与  ReentrantLock

jdk为我们提供了synchronize关键字,不考虑偏向锁和轻量锁,重量锁是依靠monitor对象实现的,那么在发生竞争的时
候,他会在hotsput里面创建一个队列(阻塞啊,等待啊),wait,notify,那里也一样.都是依赖队列
这里的aqs,他也是干这种事的.jdk中synchronize的实现方法看不到,得翻hotspot的源码,麻烦,这里可以直接看aqs的源码

源码的注释

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hpRerZ6T-1619335449443)(D:\Users\cua\Desktop\ty\aqs 原理.assets\1578115959619.png)]

aqs 的注释, 实现锁的一个框架,依赖同步器(信号量,事件,等等) 依赖一个fifo的登台队列.这个类的目的是是一个有用的基础,大多数类型的同步器.重点是 队列 信号量

我打算通过,各个组件的代码,来表达aqs的功能.例如,在reentrantLock中,aqs提供模版方法(模版方法,完成Acquire(),和Release(),并且完成状态值赋值),让子类实现,自己维护队列,来实现锁.他是怎么能把代码和功能,这么完美的拆分出来,理解不了.

1.2 aqs的组成.
1.2.1 api
aqs{
    Node{
        前后节点
        状态值
        是否共享
        线程
       nextWaiter
    }
    ConditionObject{
        等看完,实现,再去看这的api ==>位了模拟wait notify的功能准备的,另一组队列,
    }
    获得独占锁()
    获得共享锁()
    释放锁()
    cas操作()
    偏移量
    节点信息(head,tail)
    
}

太多了,就简单看一下,

1.2.2 数据结构

所有阻塞线程,抽象成一个双向链表.俩俩共享一组park,unpark.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-up4sI5XR-1619335449447)(aqs 原理.assets\1578120962188.png)]

阻塞队列的结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GuetB82n-1619335449448)(aqs 原理.assets\1578205285040.png)]

​ Condition 实现原理图

1.2.3 代码逻辑

ReentrantLock锁功能:

比如,reentrantLock,lock在阻塞的时候.将线程信息,封装成Node,加入tail后,进行park.等待前pre节点unpark.

具体细节逻辑,看代码.

在结果上看:reentrantLock实现锁的时候,只做了尝试获得锁(成功修改状态),尝试释放锁(成功,修改状态).

当然,这个获得锁,一定是cas操作. 至于,轮询,等待,都交给了aqs. ==>前者只做tryAcquire,tryRealease,并修改状态,后者解决队列的信息.

Condition 锁功能的逻辑.

先使用Condition,明白他的功能.他有几个特征,1.暂时释放锁 2.等待重新排队获得锁. aqs实现逻辑是.

当Condition,需要wait的时候(这个时候已经持有锁),那就把他从主队列拿出,排在Condition队列尾部.

当被唤醒的时候,在加入到主队列的尾部(执行signall),继续竞争锁,当然那里会重复校验,node是否在主队列中,毕竟unpark,不知道什么时候就触发了.==>首先分析,他的这个功能,也就差不多知道代码怎么写的了

2.通过ReentrantLock理解aqs

2.1 介绍功能

1.锁.

2.条件锁 Condition

2.2 自己如何设计

如果自己设计一个独占锁.(依靠状态值)

lock{
 int i=0 ;
    lock(){
        i=1;
    }
    unlock(){
        i=0;
    }
}
==> 不支持并发,可能一堆人获取到锁
lock{
 int i=0 ;
    lock(){
    while(compareandsetstate(0,1))
        i=1;
    }
    unlock(){
        i=0;
    }
}
==>支持并发,但是如果所有线程都在竞争,岂不是浪费cpu资源?yield的话,如果竞争多了,还是会浪费资源.
    
lock{
 int i=0 ;
 waitQueue   queue ;
      
   lock(){
   	while(!compareandsetstate(0,1)){//如果上锁不成功
        queue.add(thisThead)
        thisthread.park() //放出cpu
    }
}

    unlock(){
       waitQueue.getnextThread.unpark() //唤醒下一个
    }
}

==>一个接一个的使用资源,并且支持并发

如何设计一个条件锁 Condition.

1.await的时候,要释放锁.那就得唤醒队列的下一个值,并且当前线程,是放在队尾,还是,放在另外的数据结构里,如果是队尾,那么唤醒的时候,又要打破结构了.所以应该放在另外的地儿(队列).
2.当前条件 park  等待其他线程的unpark().   那就由,其他持有锁的线程,唤醒该线程.当唤醒该线程的时候,他又重新去竞争锁,那优先级是什么样的???所以,还是得加入到aqs主队列里面,重新等待获取锁.
3.一个condition,维护一个单独队列.  await 释放锁.
结论: 准备一个condition队列,当signal的时候,又重新竞争锁,在加入主队列


lock.lock()
condition1.await()
lock.unlock()


lock.lock()
condition1.signall()
lock.unlock()



2.3 aqs与其分别做了什么(代码)

翻阅了,ReentrantLock的lock(),unLock(). 对于锁这件事,

==>aqs只让reentrantLock做了两件事,尝试一次获取锁,若成功修改队列状态值.尝试一次释放锁,若成功,修改状态值.

==>aqs,操作利用子类的api,完成了,队列的吞吐.

拿出几个核心方法.,走一遍,lock() unlock()

//1.支持重入锁.
//2.释放锁的时候,只有一个线程在操作,所以没有多余的cas操作.    
//3.通过当前方法,释放锁,★改变队列状态setState
//4.tryRelease  和 tryAcquire (这里粘的是公平锁api)
protected final boolean tryRelease(int releases) {
    //计算当前的state值,重入一次+1
            int c = getState() - releases;
    //如果其他线程调用release,则会抛出异常.
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
    //如果重入锁都退出了,清空数据.
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
    //设置状态
            setState(c);
            return free;
        }


//尝试获取锁,判断状态,如果可被占用,则进行一次cas操作(尝试占有锁)
//已被占用的话,看是不是重入锁,这是reentrantLock,自己实现重入的判断.
//★通过当前方法加锁,setState (cas操作)
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }
//获取锁的过程 

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //这里判断当前节点前一个节点,这个地方会不会,有逻辑错误???
                //这里只有在,前一个节点是头节点的情况下,才开始阻塞,才进入cas(死循环获得锁,死circular在for(;;)),
                //否则就进入下面的if里面的locksupport.lock方法,进入等待了.减少不必要的空转.
                //==>牛逼.
                //疑问??? 轮询的时候如果竞争比较大,导致head节点为空呢==>是不是就进入永久等待了??==>是否node.predecessor(); 真的会抛出nullpointException.==>不可能为空,维护一个队列着呢   
                //如果 p==head,永远不成立,那他永远不会获取锁啊.......==>所以看看unpark方法,或者看看初始化方法,看他怎么处理的head节点==>head节点一直存在,获取锁了,就会set,if后面的代码有.所以这里.p==head,迟早会成功.  如果前一个节点,执行完毕,p的pre是是谁?????head是谁. 等式能否成立.就感觉p==head
                //总是会执行不成功.
                //==>如果是按照队列顺序来执行
                //那么这个等式就是成立,不论什么时候轮到这段代码执行.
                //从代码里也能看到,如果其他线程也执行到了这儿,并且先执行了下面的代码,那么,并没有改变其他节点的pre值,所以pre值一直存在,如果不存在,那就是有问题的,抛出异常. 如果head节点
                
      //感觉这里代码 逻辑不通
                //假设,在头节点,执行完毕,他没有动这个node的pre,那么p==head,就能成立,这个时候,他就可以拿资源.
                //只有老二,获得锁成功之后,才会重新删除头节点.所以,
                //我上面遇到的问题,是别的线程来操作,这个队列,导致,你自己变成了头节点,那么这个等式,可能永远不成立,==>首先这个命题是错误的,如果是,维护 一个队列来执行获取锁这个事,那么,按顺序来的话,他如果是老二(也就是即将被执行),那么就不可能别的线程来影响这段代码,因为别的线程进来就是wait(后面的partandcheckInterrupt方法)的.
                //忘了上面,是在分析个啥??? 如果aqs维护好了,这个head,以及node.pre.这里应该就没啥问题.
                //写的挺巧妙
                //p.next = null; // help GC  这个怎么理解. 平时,当一个对象中的某一部分不用了,需要被回收了,我们可以让 object==null ,但是,这一部分与原对象还是有引用的,比如a.b=xxx, map 的某一节点值置位null,但是这个数key还是存在.最好的办法就是,map删除这个对象,内部会删除这个key,这个key也就失去了map的引用,就会被回收.去除改对象和周围节点的引用,        那么在链表中呢? ,链表中, node 被上一个节点.next引用,被下一个节点.pre 引用. 我们删除这个引用, 上个节点.next=null 下一个节点.pre=null  去除引用.就帮助gc
                //p.next = null 为了帮助 node 被回收.
                //在上一轮里,p.next = null ,这一轮里, node.prev = null;所以head被回收
                // 1.判断,需要去除什么引用.  2.什么时间点去除引用.(在数据完全没有用处的时候)
                //在aqs代码执行的时候,  Node对象存在于head为起点的引用链里.当Node 不与队列关联的时候,node就相当于一个成员变量消失了,就会被回收掉.
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }


 //condition源码
 
 public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
     //同wait一样,执行await,需要释放当前锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
     //释放锁之后,node已经不在主队列中了,所以可以park(),等待signall的unpark
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                //模拟了wait的代码,可以监听线程是否被Interrupt.
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
     //acquireQueued(),代码执行到这里的是,已经被signall了,node,已经在主队列中了,执行这个方法
     //继续等待获得锁.
     //其余方法都是为了记录异常,并且完成相应的逻辑.
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }






        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

    final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * Splice onto queue and try to set waitStatus of predecessor to
         * indicate that thread is (probably) waiting. If cancelled or
         * attempt to set waitStatus fails, wake up to resync (in which
         * case the waitStatus can be transiently and harmlessly wrong).
         */
        //重新加入到队列中
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            //释放await代码的park()
            LockSupport.unpark(node.thread);
        return true;
    }
2.4 总结
1.关于公平锁,非公平锁. 
差异:让不让插队.
实现:在获取锁的时候,尝试先插一次队,在继续走aqs的代码.
效果:咋说呢,排一次队,就需要线程park(),然后unpark,如果获取锁的时候,直接获取到锁,就减少了一个线程上下文操作,挺好.如果竞争比较激烈,插队次数越多,那么效率就越高呗. 如果竞争不激烈差不多吧,毕竟nofair方法,还多执行了一遍tryAcquire呢.

2.分工: 
ReentrantLock,提供单次获取锁操作,
aqs,运用提供的方法,进行控制队列.

3.从代码上看,竞争发生在尾部,头部很少会出问题吧.头部只需要设计好逻辑代码就成.尾部也会发生竞争.
如果刚入队的哥们执行tryacquire,刚被唤醒的哥们也执行tryacquire 不就发生竞争了吗

4.关于cas操作.
Unsafe类,可以操作缓存.可以改变对象的属性值.(而且是原子操作),cas操作.==>自己敲敲api就知道了
这里面,挺多都用unsafe里面的api.
比如cas操作,compareAndSwapInt(Object obj, long offset, int expect, int update);
具体,对象某个字段的内存地址便宜量,expect,和update值.



3 通过ReentrantWriteReadLock 理解aqs.

3.1 功能

为了提高并发能力,针对同一把锁,有的时候,他的状态是可以允许几个方法同时执行,有的时候,只能有一个方法执行.但是,现在锁的功能是,我只能有一个方法执行.为了解决这种现象,发明了读写锁.

==>来满足,读读, 写读 共享 读写,写写互斥.

3.2 如何设计
为了满足上面提到的功能,我们应该如何设计.
1.现在有aqs队列,如果是读读功能,那么在竞争锁的时候,先判断队列是共享状态,还是独占状态.在执行代码.
==>那么,如果此时是读状态,如果读状态一直持续,那么写的锁,就会出现饥饿状态.
2.锁的升级,降级问题.  
我在读的时候,可以写吗? (升级),已经是读读共享了(多线程持有锁),如果都读,都要写,那就会死锁了,每个写锁,都要等待其他读锁释放.
我在写的时候,可以读吗? (降级),可以降级的,写的时候,只有一个线程持有,他进行读,不会出现死锁.
3.关于数据结构方面
如果读写锁竞争的线程都放在一个队列上.效率感觉都不会太高,
    ==>比如,当前是读锁,那么队列中的排队的都是写锁,如果当前是写锁,那么后续的线程,结构就会比较混乱,读写都有.
    ==>那么这样的情况下,代码变复杂了一些,只是提高了持有读锁的时候的并发性,可行吧.....
        
那么如果,我弄两条队列,一个写队列,一个读队列.我建立他们合适的规则.是否可以提高并发性?
    ==>比如,我在读的时候,我就可以一口气让读队列所有数据获得锁.不至于像一条队列那样,数据不均匀
    ==>写的时候,锁竞争的排队,也比较均匀.    
如果用两条队列,那么他们之间的规则是怎么样的呢?
    1.唤醒优先????当写线程操作,释放锁的时候,是唤醒读线程,还是写线程?(如果读线程释放锁,指定找写线程,因为读都执行完了)     
    2.读线程插队????? 如果锁是读线程持有,但又写线程正在等等待,让不让新来的竞争立即获得访问权限,如果可以的话,那并发性应该能提高,但是,写线程就会发生饥饿问题(现象上,有点像非公平锁的插队.)
    3.升级问题,上面已经解释了..
        
==>不知道,reentrantwritereadlock 是咋涉及的,看源码....        
        
3.3 读写锁源码的设计

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1LgrxZp4-1619335449451)(D:\Users\cua\Desktop\ty\aqs 原理.assets\1578221922903.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kLtHSFbK-1619335449452)(D:\Users\cua\Desktop\ty\aqs 原理.assets\1578390380513.png)]

3.4 aqs在里面做了什么.

对于共享锁,aqs完成了,队列操作. 完成了共享功能,和独占功能.

==>ReentrantWRLock 提供 一次获得锁的动作,释放锁的动作 (tryAcquireShared tryRelease)

这个动作,可以完成,根据队列状态值,(成功失败获得锁,分别做什么).比如你获得锁了,其实就是update state 成功. 没获得锁你就返回啥啥,你只需要提供一次动作就好,其他交给aqs 释放锁,也是update,state. 具体队列怎么运转的全部交给aqs.

==>而队列的Node的状态管理,全部由aqs完成.

3.5 部分源码解读.
//共享锁,ReentrantWriteReadLockg 一次获取锁的动作

        protected final int tryAcquireShared(int unused) {
            /*
             * Walkthrough:
             * 1. If write lock held by another thread, fail.
             * 2. Otherwise, this thread is eligible for
             *    lock wrt state, so ask if it should block
             *    because of queue policy. If not, try
             *    to grant by CASing state and updating count.
             *    Note that step does not check for reentrant
             *    acquires, which is postponed to full version
             *    to avoid having to check hold count in
             *    the more typical non-reentrant case.
             * 3. If step 2 fails either because thread
             *    apparently not eligible or CAS fails or count
             *    saturated, chain to version with full retry loop.
             */
            //如果有独占锁,并且独占锁的线程不是当前线程,则return GG
            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            
            int r = sharedCount(c);
            //如果不阻塞,就尝试一次cas操作
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                //处理缓存,以及第一读者. 缓存:每一个线程都有自己的CacheHolderCounter.
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            //如果尝试一次失败,在进行,完整的尝试获取共享锁(在没有独占锁的情况下)
            //这个和上面代码差不多,只不过是轮询,进行共享锁插队,直到获得共享锁.
            //==>允许共享锁,插队.
            return fullTryAcquireShared(current);
        }


//把state 值的前16位当做共享锁,后16位当做读写锁,从下面的位移运算就能看出来,这个操作方式,可以借鉴!!!
        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

        /** Returns the number of shared holds represented in count  */
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        /** Returns the number of exclusive holds represented in count  */
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

//readerShouldBlock()的后续方法,如果有前置结点,就阻塞==>为什么?当有前置节点的时候不让插队?
//首先这个方法是判断,当前thread之前,队列中是否有数据? 如果有,那么就不走这个代码,走后面的cas操作,插队
//进入共享锁.

// 2020.0421 ,判断,是否有等待更久的结点.  
//分析这个链表,如果有长度,那么,如果head.next!=currentThraed,就说明除,还有等待的节点.
//如果还未初始化,那么,可能就出现并发入队的现象. head先初始化,tail紧跟随后.
//如果tail==null,head!=null说明,有结点,正在入队.正在初始化,所以((s = h.next) == null成立. 如果都等于空,那么就没有等待节点. 不可能tail!=null head=null,head先初始化.
//如果,下面的 定义顺序颠倒.  也是发生并发入队,先获得head 值,这个时候tail可能初始化,完成,可能初始化未完成.这种方式写代码, h != t  捕获不到这个瞬时状态,相当于漏判.(可能h==t)
//如果,定义顺序和初始化顺序相反,先定义tail,那么 h!=t,就能捕获到,这个初始化状态
//==> h!=t     (s = h.next) == null 捕获这期间是否初始化.  
    public final boolean 
    hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        //方法的正确性,依赖于头节点比尾节点优先初始化,当前节点是补是第一个节点.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
//这段代码,在并发条件下,判断当前队列第二位是否有值(不是自己线程)
//==>如果是非并发代码,我们要确定有没有其他节点.  h是不是等于null(未初始化). 初始化了(就是当前线程在队列头) 
//==>我们来分析这段代码,
//如果t==null,head==null(跟初始化顺序有关) 		则 返回false
//		或者 head!=null(head正在被初始化,tail还没有被赋值),那么head!=tail,h.next==null,就说明有一个刚完成初始化(此时,有一个线程持有锁,另一个线程来竞争,然后初始化队列,后者还未持有锁)
	
//如果t!=null,那么head!=null(初始化完成)
//h==t 队列无等待者返回true
//h!=t 第一种情况是上面的情况,第二种情况就是,该线程达到第二位.或者其他线程到达第二位.



//Queries whether any threads have been waiting to acquire longer
//than the current thread.   这是 源码注释,判断是不是有  "等待" 更长时间的线程,强调等待状态,那就是,不是已经获得锁的人,就是判断,队列第二位,有没有其他线程.队列第一位,正在获得锁,就不算等待的node.
//当队列的饿head=tail的时候,要么,队列只有一个节点,他正在获得锁(只有获得锁的时候,才有setHead,头和尾才能相等.)
//方法的意义,就是判断,这个时候,我尝试获取锁,合不合理,如果说,我是第二位,(第一位已经获得锁,)那我就可以尝试一次,very good.
//源码的注释,很准确,后面的问题,也会依赖这个方法



//如果当前是独占锁,那么走的代码,就是ReentrantLock那一套.


 private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //如果当前节点是第二节点,才尝试获取锁,增加效率
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                     //获取锁后,设置头节点,并唤醒下一个节点,下一个节点唤醒下一个节点(如果是共享锁的话)
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                ./阻塞代码
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                //取消节点,重新拼接有效的节点
                cancelAcquire(node);
        }
    }




    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        /*
         * Try to signal next queued node if:
         *   Propagation was indicated by caller,
         *     or was recorded (as h.waitStatus either before
         *     or after setHead) by a previous operation
         *     (note: this uses sign-check of waitStatus because
         *      PROPAGATE status may transition to SIGNAL.)
         * and
         *   The next node is waiting in shared mode,
         *     or we don't know, because it appears null
         *
         * The conservatism in both of these checks may cause
         * unnecessary wake-ups, but only when there are multiple
         * racing acquires/releases, so most need signals now or soon
         * anyway.
         */
        //propagate > 0,已经持有共享锁,其他判断每看懂????????????h
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
        
        
            private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
          /**以下分析都没有错,单独写一个模块解释
          解决释放锁共享锁的问题,尽管有其他线程也在释放/获得锁.根据是否需要,释放下一个节点.
          另外,我们必须循环,防止新结点添加进来.我们需要检查cas状态.
         ★★ 关于cas操作
         ★★ 当有新节点加入的时候需要park,会把上一个节点waitStatus变为-1,可能会出现cas失败
         ★★★ 释放下一个节点可能会出现并发.   当 A线程 c==0 并释放共享锁锁的时候+后面的B节点刚被唤醒(c==1了),他们俩并发执行同下面的同一段代码,这段代码通过cas操作node的状态值,避免了重复释放.
         首先,这两段代码,并发的时候,head相同,那就是,看谁先cas操作成功了,不管谁先cas成功,都不会重复unpark.  
         假设,head.waitStatus = 0 那么第一个人就把他变为PROPAGATE状态,第二个进来的,就直接退出循环了.
         假设,head.waitStatus = -1 ,那么第一个人把他变为0,第二个人就把他变为 PROPAGATE 状态.这种抢矿可以吗????????疑问.
         关于head结点可能变掉.A,B 线程有一个线程执行了unParkSuccessor,C被Unpark(),那么head改变,那么另一个线程和C进行并行.那么就继续循环,释放下一个合适的节点.
         结论: 在并发环境下,避免了重复释放,并且让本次释放动作不失效(一定是有原因的退出循环)
         疑问: 有一个已经赋值为0 了,其他又把他赋值为-3 能行吗?
         
          */
                //问题: 这里为什么要进行cas操作的. 进行到这里了,头结点还会被改变?
                //这个问题详细说明.最后会在总结里面介绍,并用图,解释,到底哪里会发生并发操作.
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {//代表队列有数据
                int ws = h.waitStatus;
                //设置当前node节点为signal成功,才去唤醒下一个节点
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    //唤醒下一个可用节点
                    unparkSuccessor(h);
                }
                //如果头节点,不需要唤醒,那就设置当前节点为可传播状态
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }
        
   //当前节点,只会唤醒自己的下一个节点...     
==>A,B,C 三个线程(都是共享锁).
==>当 A 获得锁后,会unpark(B),
==>A,执行的快,会先执行setHeadAndPropagate[A],此时c=0.B线程被释放了还未执行c值操作,以及setHeadAndPropagate[B]
==>如果B特别慢,那么A线程就会走完setHeadAndPropagate[A],A的waitStatus值由0->-3 就走完了.(初始为0,是因为,他在获得共享锁的时候执行了unparkSuccessor(a),会将自己的waitStatus变为0)
==>如果B稍微快点,head结点被改变了(c=1),A将继续循环,这个时候.h.waitStatus = -1 或者 0(初始值为0,如果有后续结点就为-1),那么这个时候就会出现并发操作.A线程和B线程同时操作head结点,所以需要cas操作.
	如果有后续结点(h.waitStatus=-1),在cas控制下,他只能被unpark()一次.另一个竞争失败的,会执行if(head==h)中 bread; 方法退出循环.(AB有竞争,操作同一个head,所有用cas操作)
	如果没有后续结点,那么(h.waitStatus=0),那就if (h != null && h != tail) 直接退出循环了.
	如果有后续结点(h.waitStatus=-1),一个执行快,一个执行慢,那么第一个线程把h.waitStatus变为0,第二个线程把他变为-3,也是有可能的.
	如果有后续结点(h.waitStatus=0,加入到了队列中,还没有进行park,正在赋值前节点为-1),那么也能进入方法,可能会发生竞争(ABD竞争,所以用cas操作)

==>注:
1.当新节点入队的时候,会先入队(设置tail),在进行cas操作,把前节点waitStatus变为-1.
2.当unparkSuccessor(h) 会把自己的节点变为 0 也就这一个地方操作已经被唤醒节点了(在共享锁释放锁,和共享锁获得锁的时候).
3.只有操作同一个head的时候,才会出现竞争. AB竞争发生在,A释放锁,B获得锁(A轮询了一圈,或者感开始轮询就发现head已经和B一样.此时h.waitStatus不一定是多少,h.waitStatus=-1,已经有新的节点加入,此时AB竞争同时操作waitStatis等于-1; 
h.waitStatus=0,没有后续结点了初始值为0,那么在外层退出for;;
h.waitStatus=0,有后续结点,还没进行cas操作.刚入队,此时竞争,ABD,同时竞争;入队那哥们一定会一直竞争的.他那里的判断条件也是小于=0 就会尝试赋值,包含了这种情况,所以很nb nb.
4.当B线程执行的时候,就代表有C不存在(waitStatus=0),或者C存在是读线程(waitStatus=-1).

那么这个方法的目的是什么?
cas操作成功,释放head的下一个线程.如果没有下一个线程,则设置当前线程为可传播状态,并且解决竞争问题!!!
continue的逻辑是为了完成每一个动作,让他满意离开,如果cas失败,就再来一次,无非就是0,-1两种情况.

那这段代码,是怎么设计出来的?????
1.为了处理队列下一个值,如果有,则解锁,如果没有则标记状态.
2.那么这个过程,会有竞争吗???  释放锁和获得锁有竞争              刚入队的时候,和前两者都有竞争.ws=0
如何处理,cas操作,竞争成功的线程,才能做动作.竞争失败,退出循环.
如果顺序执行呢,node结点就变成传播状态.

==>结论:脑袋不够,设计不出来.

1.先明确要干什么
2.解决了什么样的竞争.
3.完善每一个竞争,每一个流程的结果.

参照,上面的图

    final int fullTryAcquireShared(Thread current) {
            /*
             * This code is in part redundant with that in
             * tryAcquireShared but is simpler overall by not
             * complicating tryAcquireShared with interactions between
             * retries and lazily reading hold counts.
             */
        //读线程缓存计数
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                    // else we hold the exclusive lock; blocking here
                    // would cause deadlock.
                    //判断前面是否有等待时间更长的读线程,如果没有,cas操作,获取一次锁
                } else if (readerShouldBlock()) {
                    // Make sure we're not acquiring read lock reentrantly
                    if (firstReader == current) {
                        //支持重入锁
                        // assert firstReaderHoldCount > 0;
                    } else {
                        if (rh == null) {
                            //这个东西,一直是记录,最后一个操作的线程
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                    //仅仅情空空当前线程的数据
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)
                            //队列中还有等待更长的结点,所以去尾部插队去
                            return -1;
                    }
                }
                //当没有等待更长的结点,那就正常cas操作,并且记录缓存数据.
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }
3.6 总结
1.发生竞争的点在哪里?
凡是有cas代码的地方,都有可能发生竞争.
==>读锁,发生在读锁持有的时候,"插队读",以及队尾的写线程
==>写锁,主要发生在队尾入队.
2.关于waitStatus,初始为0,有nextNode,并且需要park的时候,会编程-1(signall),当准备unpark的时候,会变成0.
3.调试完代码,又另一种看法,关于读写锁的运行效率.   
    这么一种代码现象,如果头节点()阻塞,此时没有队列,是否可以有读线程进来,正常逻辑的话,我们只能通过释放锁唤醒,或者读取锁的过程,唤醒下一个读线程.这里读线程就可以直接加入了,并且不需要进行入队操作,原因是,在读写锁的tryAcquireShare,里面,有判断,你是否有等待时间更长的节点,如果没有,就可以尝试一次入队. 并且这操作,不需要添加到"尾节点",贼快.(这设计也太nb了)
    ==>针对,无队列时,读节点获得锁的时候,可以无限插队.
    ==>如果,有队列呢,如果读队列后面有写队列,那就插入不进去,如果读队列后有读队列,那么这个时候,根据代码,如果有等待时间比你长的读线程(就是说队列第二位有人,读线程还没释放完),那么这个时候,你尝试获得共享锁,你会尝试两次,加入队列(当然这个时候支持重入锁的),try一次,fully一次,如果没加入进去,就加入到队列尾部.
    ==>当然,这段时间非常短,当所有的读线程都释放完,那么head节点一直变化,直到最后一个读节点的时候,这个时候,就没有比你等待更长时间的读节点,这个时候,就会插入成功.    
    ==>照顾比你等待时间更长的读节点,并且还支持你的"插队"!!!!
    ==>读锁的,顺序又保障,速度也快.

4. 通过BlockingQueue理解.

4.1 功能

1.简单翻阅BlockingQueue 接口的注释,和其他容器一样,有添加,删除,"奉献"技能(鼬给佐助).而且注释上面有一个简答的,producer-consumer模型的代码.

某些api,看注释,翻译一下.

add(obj):往队列里面增加一个对象,如果队列没有空间抛出异常,反之返回true。

offer(obj): 往队列增加一个对象,返回true/false

put(obj): 往队列增加一个对象,如果没有空间,则会阻塞改线程,直到有空间.

注:一般返回void,都是阻塞的. 返回boolean的,都是有非阻塞的.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vK39zprB-1619335449453)(D:\Users\cua\Desktop\ty\aqs 原理.assets\1578537106949.png)]

BlockingQueue提供基本功能,实现者实现基本功能就ok.

ArrayBlockingQueue:
数组形式的阻塞队列.如果是阻塞的话,那就是这个容器,在full,Empty是会阻塞,等待唤醒.producer-consumer模型呗.api同Blocking相似.
迭代功能???

LinkBlockingQueue:
链表形式的阻塞队列.也有Blocking的功能.producer-consumer的模型.实现上未知.有什么设计特点,看完源码在写.
迭代功能????



4.2 如何设计
  ArrayBlockingQueue
  应该比较简单
1.首先这是一个 producer-consumer模型 的容器 
2.创建一个Object[],一把锁,对所有添加,删除,读上锁.
3.针对full,Empty的情况,建立条件锁.
	注:如果不用条件锁,那就用老的synchronize,wait,notify.
	   读也要上锁吗
container{
    lock lock
    condition notempty
    condition notfull
    
    take(){
    lock.lock()
    
    isEmpty()	
    notEmpty.await()
    take
    notfull.signall
    
	lock.unlock()    
    }
    
    poll(){
    lock.lock()
    isfull notfull.await
    poll
    notEmpty.signall
	lock.unlock()       
    }
}	   
  LinkBlockingQueue
如果,我自己设计,估计和Arraybq的想法一样,一个容器,一把锁,俩条件,控制操作,


1.首先这是一个 producer-consumer模型 的容器 
2.创建一个Link
3.针对full,Empty的情况,建立条件锁.



4.3 源码设计.
ArrayBlockingList. 

基本原理:同上面的设计想法一样.

其他设计:

1.添加了指针,方便操作数组.fifo(双指针)

2.这个数组是如何存取的,(哪存,哪出.)依赖1,指针.

==>我理解他是一个★★双指针★★,起始都是头结点,两个指针都是从0->max,当到max之后,0继续.而队列的出入队,跟着指针走就行,这样就做到了,fifo队列.

==>这不就以前操作链表的一种方式吗,以前写线程池源码的时候,也这样弄过线程数组.

==>★★★简单来说,就是放入的顺序,和取出的顺序一致就ok.★★★

3.他的迭代器是如何工作的.

如果多个线程要迭代,那就创建多个iterator(),itr内部类,itrs就是这些迭代器的一个链表.
==>1.为什么要自己实现呢迭代器?
因为对于blq,迭代发生的时候,可能数据已经被改变了,每一个迭代过程中,执行时间不同.所以很容易,获得的数据不是那么准.
==>2.itr是个啥?
这里的迭代器,依赖指针来实现,因为指针是相当于数据而言,不是数据而言,不会出现nullpe的问题,其他迭代器不知道.
                    cursor = NONE;
                    nextIndex = NONE;
                    prevTakeIndex = DETACHED;
比如这样,他在创建迭代器的时候,会记录当时起始指针的位置,然后,依靠这个便利.当然这部分数据里面,有修正的代码,比如,有数据删除,他就通知所有的迭代器,修正一些.具体的不想研究,因为这本身获得的就不是一个准的数据,在并发条件下不是特别准,并不能说明什么.
==>3.需要使用迭代器的时候,再去研究.或者等看看其他源码用到blq的时候,他是如何使用迭代器的(我看看那帮孩子咋玩的)
LinkBlockQueue
基本原理.

1.两把锁(入列,出列各一把,NotFull_NotEmpty),
==>对于Array那种,全局一把锁,可能某些情况下,并发性不如两把锁.
==>应该是比较 出列 和 入列速度.不能这么说,生产,消费模型,不就为了让吞吐率更高吗?如果速度有差异,那就得口控制下两者的速度,或者制定啥规则,让生产速度合理.  比如,我先多开点线程,多生产一些,在控制消费和生产的效率在一个level,就等待少,还能消费的快.消费能力的提高,那么锁的竞争就会比较激烈,毕竟就一把锁
==>搞两把锁,在不考虑full,和empty的情况下(一直有资源),竞争减少了一半.加快了并发性.
就是一些操作的难度增大了,比如迭代(需要获得两把锁),或者其他什么情况,需要获得两把锁,代码的严谨性需要提高,
==>尽量不要做一些,需要两把锁的操作★★

2.迭代器
==>我看到了,使用了全局锁,而且迭代过程中,也用了全局锁(俩锁一起获得)
==>支持SplIterator,支持多核电脑呗
==>其他业务不想细看.
4.4 部分代码

ArrayBlockingQueue

//有返回值的入队    
public boolean offer(E e) {
        //不支持空元素
        checkNotNull(e);
        //获得全局锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
//入股full 则返回false.            
            if (count == items.length)
                return false;
            else {
                //入队
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }
//为什么要单独提出来一个瞬时值.
//因为cas 操作,需要一个期待值,和变更值,来完成一个cas操作. 如果这个期待值,是个变量就没有意义.
//我拿出来一个瞬时值,获得期待值,然后进行cas操作,竞争,如果竞争成功,就代表我这段代码操作符合逻辑.是原子性的.
//所以aqs代码,很多都是这种形式,我们自己写并发代码的时候,也会经常用.

    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        //这里为什么要拿出来一部分瞬时值,未知..(这个时候已经获得锁了啊)????????
        final Object[] items = this.items;
        //通过put指针入队
        items[putIndex] = x;
        //如果指针到达尾部,再从头部放进去
        if (++putIndex == items.length)
            putIndex = 0;
        //计数
        count++;
        notEmpty.signal();
    }


//阻塞的入队
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }


//迭代器的方法
        Itr() {
            // assert lock.getHoldCount() == 0;
            lastRet = NONE;
            final ReentrantLock lock = ArrayBlockingQueue.this.lock;
            lock.lock();
            try {
                //如果没数据,维护一下指针的数据
                if (count == 0) {
                    // assert itrs == null;
                    cursor = NONE;
                    nextIndex = NONE;
                    prevTakeIndex = DETACHED;
                } else {
                    //获得当前出队指针,记录下当前的迭代器需要的指针
                    final int takeIndex = ArrayBlockingQueue.this.takeIndex;
                    prevTakeIndex = takeIndex;
                    nextItem = itemAt(nextIndex = takeIndex);
                    cursor = incCursor(takeIndex);
                    if (itrs == null) {
                        itrs = new Itrs(this);
                    } else {
                        itrs.register(this); // in this order
                        itrs.doSomeSweeping(false);
                    }
                    prevCycles = itrs.cycles;
                    // assert takeIndex >= 0;
                    // assert prevTakeIndex == takeIndex;
                    // assert nextIndex >= 0;
                    // assert nextItem != null;
                }
            } finally {
                lock.unlock();
            }
        }


 itrs.doSomeSweeping(false);  维护迭代器数据的方法 //懒的看了,看注释上面的意思是,尽力找到过期的数据?
  //   还是说,换一些数据,更新迭代器?   ==>删除一些没有废弃的迭代器.

     //扫描过期的迭代器,置位null,并且重新更新node节点,前后信息
     //需要了解迭代器和这个方法的关系,在新加入迭代器节点和迭代的时候,会进行这个方法.所以,迭代器进行到哪一个点,不知道,sweeper记录一个节点..
  void doSomeSweeping(boolean tryHarder) {
            // assert lock.getHoldCount() == 1;
            // assert head != null;
            int probes = tryHarder ? LONG_SWEEP_PROBES : SHORT_SWEEP_PROBES;
            Node o, p;
            final Node sweeper = this.sweeper;
            boolean passedGo;   // to limit search to one full sweep //完整的扫描
			//上次扫描到的点
     		//给两个指针赋值
     		//根据上次的位置,判断是否需要完整扫描
            if (sweeper == null) {
                o = null;
                p = head;
                passedGo = true;
            } else {
                o = sweeper;
                p = o.next;
                passedGo = false;
            }

            for (; probes > 0; probes--) {
                //下一个节点(第二个指针)为空
                if (p == null) {
                    if (passedGo)
                        break;//说明当前没数据,退出
                    o = null;
                    p = head;
                    passedGo = true; //已经没数据了,需要重头扫了
                }
                final Itr it = p.get();//第2个指针的node
                final Node next = p.next;.//第二个指针的下一个node,先把值拿出来,方便赋值
                    //判断节点是否还有效,无效,则清除状态,判断是否需要退出循环
                    //继续向下循环,不想看了.
                if (it == null || it.isDetached()) {
                    // found a discarded/exhausted iterator
                    probes = LONG_SWEEP_PROBES; // "try harder"
                    // unlink p
                    p.clear();
                    p.next = null;
                    if (o == null) {
                        head = next;
                        if (next == null) {
                            // We've run out of iterators to track; retire
                            itrs = null;
                            return;
                        }
                    }
                    else
                        o.next = next;
                } else {
                    o = p;
                }
                p = next;
            }

            this.sweeper = (p == null) ? null : o;
        }    
     
     

LinkBlockQueue

//阻塞入队
    public void put(E e) throws InterruptedException {
        //空
        if (e == null) throw new NullPointerException();
        //好习惯,所有的takeput 方法 都提前准备好变量
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        //put锁,上锁.
        putLock.lockInterruptibly();
        try {
            /*
             * Note that count is used in wait guard even though it is
             * not protected by lock. This works because count can
             * only decrease at this point (all other puts are shut
             * out by lock), and we (or some other waiting put) are
             * signalled if it ever changes from capacity. Similarly
             * for all other uses of count in other wait guards.
             */
            while (count.get() == capacity) {
                notFull.await();
            }
            //入队
            enqueue(node);
            //加1
            c = count.getAndIncrement();
            //判断容量,如果没有put没满,唤醒自己的队友
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            //解锁
            putLock.unlock();
        }
        //这里有疑问,正常应该,直接唤醒notempty就ok了呗.
        //这里的意思是,当队列为空的时候,唤醒非空.(消耗队列),因为现在已经添加进去了一个.
        //在简易点说,就是,c>0,就没阻塞的.如果c=0 就说明,指定有阻塞的,现在添加成功了,改唤醒他们了.
        //这样增加执行效率(没那么多唤醒了,就没那么多take锁的竞争了),增加代码的执行效率.
        if (c == 0)
            signalNotEmpty();
    }
//入队,就比较简单了.
    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }


//非阻塞入队
 public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        if (e == null) throw new NullPointerException();
        long nanos = unit.toNanos(timeout);
        int c = -1;
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            while (count.get() == capacity) {
                if (nanos <= 0)
                    return false;
                //模拟wait(3000) 的功能,这段时间内释放锁,时间到后,加入竞争
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(new Node<E>(e));
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return true;
    }

//aqs的等待 方法
//利用Locksuppots 方法,控制队列
 public final long awaitNanos(long nanosTimeout)
                throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
     //添加节点
            Node node = addConditionWaiter();
     //释放锁
            int savedState = fullyRelease(node);
     //计算超时时间
            final long deadline = System.nanoTime() + nanosTimeout;
            int interruptMode = 0;
     //判断是否在主队列中,signall方法,会把合适的waiter加入到队列中.
            while (!isOnSyncQueue(node)) {
                //如果超时时间到了,就取消等待
                if (nanosTimeout <= 0L) {
                    transferAfterCancelledWait(node);
                    break;
                }
                if (nanosTimeout >= spinForTimeoutThreshold)
                    //在规定时间内,不尝试获取锁,超时过后,开始主动获取锁
                    LockSupport.parkNanos(this, nanosTimeout);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
                nanosTimeout = deadline - System.nanoTime();
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
            return deadline - System.nanoTime();
        }

//另外,关于,await(3000) 理解. 知识代表,3秒里,你不参加竞争,3秒过后加入竞争,等待signal唤醒,而signal唤//醒,只代表一个或者一群,被唤醒,这帮东西,重新加入竞争. aqs完全实现了synchronize那家伙提供的功能.挺厉害 


//关于locksuport 中这个
    public static void parkNanos(Object blocker, long nanos) {
        if (nanos > 0) {
            Thread t = Thread.currentThread();
            //给 thread 对象 设置属性值,方便管理线程.
            setBlocker(t, blocker);
            //native方法,操作系统的一个啥,一个"许可"指令
            UNSAFE.park(false, nanos);
            //设置完事了,在清空一下呗.
            setBlocker(t, null);
        }
    }




//出列的方法

    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        //同上面一样,为了代码效率,第一,执行唤醒生产线程,需要获取对方的锁,第二,我只在队列满的时候,告诉他我,继续生产,如果队列没满,他自己也会一直很生产.
        //这样减少了锁竞争, 唤醒自己锁的线程的主要任务都给了自己. 只有特殊情况(临界值的时候,只有临界值的时候,他自己不能唤醒自己),对方才唤醒他一次.  
        //只有临界情况才有锁的竞争.
        if (c == capacity)
            signalNotFull();
        return x;
    }








5. 通过其他功能性队列了解aqs

5.1 通过 CountDownLatch
1.功能,countdown()一定次数,则唤醒await线程.
2.从功能上,看比较简单.
==>实现上,用aqs队列,共享锁功能
==>构造器,给定初始state,值.
==>wait上锁.
==>每次countdown(),减少一次状态值,到达次数之后,唤醒await线程.
5.2 通过 Semaphore
和上面的countdownlatch实现原理差不多.都是利用共享锁,创建一个初始值,然后控制值变化就行了,至于队列的状态,竞争,全部交给了aqs.
5.3 通过LinkedTransferQueue
简单来说:
功能上讲,他是一个超集,有很多api.
效率上讲,他是无锁操作,入队非阻塞算法,操作头尾节点的算法很nb.
无锁:cas操作.
非阻塞算法,入队,不会阻塞.1.无边界    出队,不一定
关于出入队的算法: 以前对于队列,我们每次出队入队,需要利用cas操作更新相应的头尾节点,如果实时更新,每次都需要进行大量的cas操作,而这一次,我们延迟更新,当head节点,距离未被匹配的节点,到达两个节点的时候,才会更新头节点.当然,如果未匹配节点长了的话,你不更新,我每次都需要从头去找匹配的的空节点,或者尾节点,效率也不高.所以,他们用2


concurrentQueue,算法上和他相似,但是功能比他简单,所以在使用上比较方便.


功能:
算法设计:
1.队列结构:
一个队列.所有的生产队列,和请求队列,都在一个队列上,同为Node,只不过他们的状态值不同.
例如,初始的生产数据,Node,的isdata为true,item为object,初始的消费request,isdata为false,item为null.
数据结构:全部数据在一起,利用属性值,表达每一个节点的信息(生产node,还是消费node)


2.head节点设计.
我们在出队入队的时候,头尾结点的更新,以前的做法是利用cas操作,实时更新.
我们以前的head节点是实时的,比如,以前的队列,如果head结点释放锁了之后,就会更新head的节点,现在不会了.
而是一种延迟更新的感觉.
当head节点,距离未被匹配的节点,到达两个节点的时候,才会更新头节点.
这么做的目的是什么?  以前操作一次,就要进行一次cas操作,现在不一样了,相当于,我匹配两个进行一次.
==>这个延迟更新的东西,有将就,如果短了,就像以前的操作一次,一回合cas,导致自旋过多.
   						如果长了,意味着,我每次新加入的节点都需要从头匹配,而且前几个都不太好使,那么我循环的次数也就多了.不容易命中.也不太好.
==>这就是多次cas操作,和多次每个人的索引次数之间的一个抉择.==>这是人想设计的东西吗?   				==>而且在不更新头结点的情况下,还能保证fifo,以及线程安全,这特么是人写的代码吗??

3.这里面没有用到aqs,不过是另一种队列同步队列的实现.也是依赖cas操作,来实现的.好牛逼.


4.进行简单的,代码测试,测试了,linkedtransferqueue,arrayblockqueue,linkblockqueue,第一个的吞吐率,很强.

5.有个特点,有点像接收生产者的意思,无限生产
如果你是放数据的,永远成功.一直是true.就添加到队列中了,等待那边消费.
(和request相反,别想歪了.)而这里面的request,就是take代码
    public boolean offer(E e, long timeout, TimeUnit unit) {
        xfer(e, true, ASYNC, 0);
        return true;
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A7qZQAOI-1619335449455)(D:\Users\cua\Desktop\ty\aqs 原理.assets\1578903244295.png)]

网上找的图.但是代码确实是好生涩…看了整整一个上午,才把一个方法看完.而且不打算看其他方法了.不是人脑写的代码.

//tansferQueue,所有的take,put 方法,都是依赖这个核心方法写的,所有内容,全都揉在一起了.很是nb,而且没有锁的(没有依赖aqs,)
//hava data,表示传进来的这个节点是否有数据,如果有数据则代表他是,生产的.否则为request,消费者,相对应的,E也为null.
private E xfer(E e, boolean haveData, int how, long nanos) {
    //如果.是生产者,但是数据为空,则抛空,==>不让你产生空数据
    if (haveData && (e == null))
        throw new NullPointerException();
    Node s = null;                        // the node to append, if needed
    
	//标记一个位置,下面continue retry,会返回到这里
    retry:
    for (;;) {                            // restart on append race
//头结点不为空,才循环
        for (Node h = head, p = h; p != null;) { // find & match first node
            //这里两个赋值是有讲究的,参照上面
            boolean isData = p.isData;
            Object item = p.item;
            //item!=p 我这里没懂,item是数据项,p是node项,他们俩没有可比性啊???????????
            //右面判断有两种情况,这里是找到,未匹配的.
            //一种是,take,isdata为false,item为null
            //另一种是,put,isdata为true,item为object
            if (item != p && (item != null) == isData) { // unmatched
                //isData代表的动作和havaData(方法带进来的本结点的动作).
                //如果头结点动作相同,则不匹配,跳出循环.(这里只看头节点是否匹配,不匹配就走下面代码,然后加入队列),也有可能头节点匹配,但是cas失败,那就再找下一个节点在继续尝试,可能head就变了.
                if (isData == haveData)   // can't match
                    break;
                //如果可以匹配,那么就进行cas,操作
                if (p.casItem(item, e)) { // match
                    //如果cas成功,则唤醒线程,并看情况更新头节点
                    //按照for循环体的判断, q=p=head, 如果q!=h,那么就是,进行了第二轮循环了呗.
                    //那么现在 q 和head 都已经被cas成功了,根据注释里面的思想,当head节点距离下一个未匹配的结点达到两个,就得更新头节点了
                    for (Node q = p; q != h;) {
                        Node n = q.next;  // update by 2 unless singleton
                        if (head == h && casHead(h, n == null ? q : n)) {
                            //让head节点.next=自己.  之后就被被gc标记住了,等待回收
                            h.forgetNext();
                            break;
                        }                 // advance and retry
                        //下面这个情况比较极端,就是cas失败,或者h节点已经被改变.原来的head节点可能已经被回收掉.head的一下个也被回收掉,因为别人cas成功了,head节点一更新,直接就是第三个结点为头节点.第三个方法,q没有被匹配,但是头节点也被回收掉了,就需要重新循环了(就是说,第4个结点进行cas操作,他成功了,更新了头节点,并且这个时候,就跳出循环,先把本次cas成功的数据解决掉,唤醒该唤醒的)
                        if ((h = head)   == null ||
                            (q = h.next) == null || !q.isMatched())
                            break;        // unless slack < 2
                        //注释上面说 slack 松弛度小于2 ,也就是head结点已经被第4个结点更新了, 第二位为null被回收了,可能为null.类似于这种情况,同时竞争一个节点,结果只有一个cas成功,然后另一个就cas下一个成功了,但是后一个cas成功的,又先执行cas,head,所有会出现这几个为null的情况.
                        //q.isatched,代表,上一个还没有cas成功,方法里面判断两种情况,如果是数据项,
                        //就判断他 ((x == null) == isData); isdata为true,并且item已经被匹配,为null
                        //另一种是非数据项,他判断 item==自己==>这特么在哪赋值了????首先,如果是take方法,他进来==>找到了,在下面的awaitMatch 方法里面,如果还没匹配,会将非数据节点的item,指向自己,里面有一个赋值方法.
                        //这个情况就是发生在被后面cas成功.
                    }
                    //解锁等待的线程.
                    LockSupport.unpark(p.waiter);
                    return LinkedTransferQueue.<E>cast(item);
                }
            }
            Node n = p.next;
            //可能更换了头节点,后面,已经没有结点了,所以需要重新赋值一下
            p = (p != n) ? n : (h = head); // Use head if p offlist
        }

        if (how != NOW) {                 // No matches available
            if (s == null)
                s = new Node(e, haveData);//添加结点,里面代码挺麻烦的,不看了
            Node pred = tryAppend(s, haveData);
            if (pred == null)
                //竞争失败,就重来呗.
                continue retry;           // lost race vs opposite mode
            if (how != ASYNC)
                //非异步的就等待.
                return awaitMatch(s, pred, e, (how == TIMED), nanos);
        }
        return e; // not waiting
    }
}

6.如何使用队列.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eoDNRAK3-1619335449456)(D:\Users\cua\Desktop\ty\aqs 原理.assets\1583395038122.png)]

linkedtransferQueue

concurrentLinkedQueue 多用这个吧,api简单 算法同上.

7.aqs 攻略

从解决问题的思路来看待锁.

独占锁 部分
1.如何实现锁功能的.

目的: 只有一方占有.

2.如果自己设计

自己实现一个锁,需要考虑什么问题?

依赖什么来表达持有锁?   	
    ==>lock中的什么变量,当做标志.

竞争的资源如何表达?     
    ==>当有n多个线程竞争的时候,未获得锁的资源,如何表达.

竞争资源如何有效率的拿到锁?    
    ==>随机唤醒? sleep(time)? yield?
    
竞争资源的操作,用什么控制?    并发入队,并发初始化,并发修改状态...
状态值
1.依赖状态值,表达锁的状态
class  MyLock{
        int state =0 ;
   void lock(){
        state =1;
   }
   void unlock(){
        state =0;
   }
}
注:当然也可以用其他标志量,比如 true false .假设依赖true,false,如何设计可重入锁,如何设计共享锁???  ==>这里可以就把当做一个标志量,表达锁的状态.
乐观锁->cas,解决资源竞争问题
2.state为共享数据.

class  MyLock{
        int state =0 ;
   void lock(){
       if( unsafe.compareAndSwapInt(this, stateOffset, 0, 1) );
       	//getLock
   }
   void unlock(){
  if( unsafe.compareAndSwapInt(this, stateOffset, 1, 0));
        //unlock
}

锁的思想
悲观锁:锁住资源.
	==>其他竞争者,会阻塞.
	
乐观锁:不锁住资源,检测资源变化.
    资源变化,代表这个操作非原子,所以本次操作失败.
    如果资源未变化,代表操作原子,本次操作成功
   	==>非阻塞算法
   	
   	
sql:
悲观锁:    
select * from article_info where id=1 for update  //行锁
update article_info set count=count+1;
    
乐观锁:   
select count from article_info where id=1   
update  article_info set count=count+1 where count=count;  

增加竞争条件后.分析二者的执行情况
    
i++:
见代码;






队列与唤醒机制
3.发生竞争,未获得锁的一方,如何操作? 
    // while(true)   sleep   yield   wait-notify  ?  


class  MyLock{
  int state =0 ;
  
   void lock(){
     while(true){
 		 if(state==0){
     		 state =1;//cas 
     		 //上锁成功
  		 }
  //sleep  yield   wait-notify	等待获得锁
   }
 }
 
  void unlock(){
   if(state==1){
    state =0;//cas
   }
}

==>为了不持续消耗资源,可以让线程先"休息"类似于 wait notify的方式.
==> UNSAFE.park();     依赖于信号量的 一种, 等待唤醒机制.(操作系统提供的一种原子性操作)
==>什么时候唤醒.
==>维护一个队列,lock维护一个队列,让所有竞争方休息,节省资源.当需要唤醒的时候唤醒.
==>Node->Node->Node->Node.    当然不能像Synchronize那样,全部唤醒,在一起竞争资源.
前节点,唤醒后节点.

开始讲源码

3.aqs是什么
1.aqs是一个用来实现锁功能的一个框架. 他内部维护了一个fifo队列. 从功能上讲对外提供操作state的api,对内准备维护队列逻辑的api.
 ==>把占有锁,释放锁,抽象成一件事(等价于操作状态值,然后aqs还提供api),一次竞争事件,然后自己代码里还能合理利用这个方法.(锁就提供一次操作的api,其他任务交给aqs)

2.难点,在于基本所有地方的代码都带竞争性. 所以,得很严谨,敲的时候,一定很严谨.

翻阅过程中: 重点在,
如何表达锁.
在并发条件下,如何保证线程安全.
使用锁的时候流程.
哪里高效解决问题.

当这些都能理解了之后,再去提会代码的编程设计.
4.condition 锁,是如何实现的呢.

从功能上讲. 他想要像synchronize里面的wait,notify 功能一样. 提供线程的等待唤醒功能.
当然等待唤醒都需要依赖锁,而condition,就是aqs配套的 wait,notify功能.

==>从aqs设计出来的效果上,以及逻辑上讲是这样的
从功能上讲,waitnotify需要在同一把锁下,并且获得,锁.使用condition一样. 而且这个condition 设计出来之后,要比synchronize的要nb, 那个只有一种wait,这个可以有多个condition.
从调用方式上来看.和synchronize的基本一样.
从逻辑上讲. 每次await,会退出主队列,在维护一个队列,notify的时候,在重新加入主队列继续竞争.

==>那么是怎么设计出来的呢. 如果要你设计一个wait notify功能,你如何设计.

直接从功能上分析

wait方法的目的是 释放锁(出主队) 暂停线程==>park()? 等待唤醒
notify的目的是 唤醒某个线程==>unpark() 继续加入竞争,就是要入主队.

如果,有多个线程在同一个condition wait? ==>维护同一个 waiter 队列,

wait(){
Release()
park()

}

notify(){
unpark()
addwaiter()==>竞争到锁,会执行.
}

共享锁部分

直接从功能出发,去完成.

读读读写写写读读

在已知,独占模式的功能设计之后,我们这个共享锁,需要设计什么功能呢.怎么实现呢?
1.读读 写读 读写他们之间的逻辑关系.
2.是否支持锁的升降级.
如何实现.
1.直接去分析,比如读读功能. 如果已经拼好的队列里.读读允许运行. 那么就让读线程unpark() 下一个线程.
写互斥,就和原来一样.
2.那么插队问题,如何解决. 写的话,排队.比如现在正在执行读线程,读线程插队如何解决.

源代码是如何实现的?
针对已存在的队列,读线程会唤醒下一个读线程. 不允许太嚣张的读写插队. 非公平锁的实现方法差不多.

从功能上讲,我们可以用普通的逻辑,完成代码.但是,在并发环境下,要保证,这个lock不出现线程安全问题,并且实现功能而且要精简.,确实有难度.

}
}

==>为了不持续消耗资源,可以让线程先"休息"类似于 wait notify的方式.
==> UNSAFE.park(); 依赖于信号量的 一种, 等待唤醒机制.(操作系统提供的一种原子性操作)
==>什么时候唤醒.
==>维护一个队列,lock维护一个队列,让所有竞争方休息,节省资源.当需要唤醒的时候唤醒.
==>Node->Node->Node->Node. 当然不能像Synchronize那样,全部唤醒,在一起竞争资源.
前节点,唤醒后节点.






**开始讲源码**  

##### 3.aqs是什么

1.aqs是一个用来实现锁功能的一个框架. 他内部维护了一个fifo队列. 从功能上讲对外提供操作state的api,对内准备维护队列逻辑的api.
==>把占有锁,释放锁,抽象成一件事(等价于操作状态值,然后aqs还提供api),一次竞争事件,然后自己代码里还能合理利用这个方法.(锁就提供一次操作的api,其他任务交给aqs)

2.难点,在于基本所有地方的代码都带竞争性. 所以,得很严谨,敲的时候,一定很严谨.

翻阅过程中: 重点在,
如何表达锁.
在并发条件下,如何保证线程安全.
使用锁的时候流程.
哪里高效解决问题.

当这些都能理解了之后,再去提会代码的编程设计.










##### 4.condition 锁,是如何实现的呢.

从功能上讲. 他想要像synchronize里面的wait,notify 功能一样. 提供线程的等待唤醒功能.
当然等待唤醒都需要依赖锁,而condition,就是aqs配套的 wait,notify功能.

==>从aqs设计出来的效果上,以及逻辑上讲是这样的
从功能上讲,waitnotify需要在同一把锁下,并且获得,锁.使用condition一样.  而且这个condition 设计出来之后,要比synchronize的要nb, 那个只有一种wait,这个可以有多个condition.
从调用方式上来看.和synchronize的基本一样.
从逻辑上讲. 每次await,会退出主队列,在维护一个队列,notify的时候,在重新加入主队列继续竞争.

==>那么是怎么设计出来的呢.  如果要你设计一个wait notify功能,你如何设计.

  直接从功能上分析  

  wait方法的目的是     释放锁(出主队)   暂停线程==>park()?  等待唤醒
  notify的目的是       唤醒某个线程==>unpark()  继续加入竞争,就是要入主队.

  如果,有多个线程在同一个condition  wait? ==>维护同一个 waiter 队列,

  


wait(){
Release()
park()

}  

notify(){
unpark()
addwaiter()==>竞争到锁,会执行.
}

####  共享锁部分

 直接从功能出发,去完成.

读读读写写写读读

在已知,独占模式的功能设计之后,我们这个共享锁,需要设计什么功能呢.怎么实现呢?
1.读读 写读  读写他们之间的逻辑关系.
2.是否支持锁的升降级.
如何实现.
1.直接去分析,比如读读功能. 如果已经拼好的队列里.读读允许运行. 那么就让读线程unpark() 下一个线程.
写互斥,就和原来一样.
2.那么插队问题,如何解决. 写的话,排队.比如现在正在执行读线程,读线程插队如何解决.

源代码是如何实现的?
针对已存在的队列,读线程会唤醒下一个读线程. 不允许太嚣张的读写插队.  非公平锁的实现方法差不多.


从功能上讲,我们可以用普通的逻辑,完成代码.但是,在并发环境下,要保证,这个lock不出现线程安全问题,并且实现功能而且要精简.,确实有难度.












  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值