AQS详解

AQS的介绍
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。
AQS详解_第1张图片
AQS原理
AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态(解释:就是通过volatile int state这个变量来判断该资源是否可以被当前发起资源请求的线程占用,如果state>0,那么就说明已经有线程在占用该资源了,其他线程就不能进行访问,除非state变成0,才可以申请访问该资源),如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例(解释:不是真的用队列Queue去存储这些节点,而是通过某种方式维持节点之间的关系,从而使得看起来像队列),仅存在节点之间的关联关系。

看个AQS(AbstractQueuedSynchronizer)原理图:
AQS详解_第2张图片
AQS详解_第3张图片
AQS详解_第4张图片
AQS详解_第5张图片
模板方法模式链接:
https://blog.csdn.net/qq_40241957/article/details/84898404
AQS详解_第6张图片
AQS详解_第7张图片
解释: ReentrantReadWriteLock是对“读操作”共享,“写操作”互斥的

自定义同步器
  同步器代码实现

上面大概讲了一些关于AQS如何使用的理论性的东西,接下来,我们就来看下实际如何使用,直接采用JDK官方文档中的小例子来说明问题
AQS详解_第8张图片
解释:下面的Sync类对象是上面定义的静态内部类
AQS详解_第9张图片
同步器代码测试
测试下这个自定义的同步器,我们使用之前文章中做过的并发环境下a++的例子来说明问题(a++的原子性其实最好使用原子类AtomicInteger来解决,此处用Mutex有点大炮打蚊子的意味,好在能说明问题就好)

package juc;

import java.util.concurrent.CyclicBarrier;

/**
 * Created by chengxiao on 2017/7/16.
 */
public class TestMutex {
    private static CyclicBarrier barrier = new CyclicBarrier(31);
    private static int a = 0;
    private static  Mutex mutex = new Mutex();

    public static void main(String []args) throws Exception {
        //说明:我们启用30个线程,每个线程对i自加10000次,同步正常的话,最终结果应为300000;
        //未加锁前
        for(int i=0;i<30;i++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i=0;i<10000;i++){
                        increment1();//没有同步措施的a++;
                    }
                    try {
                        barrier.await();//等30个线程累加完毕
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
        barrier.await();
        System.out.println("加锁前,a="+a);
        //加锁后
        barrier.reset();//重置CyclicBarrier
        a=0;
        for(int i=0;i<30;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i=0;i<10000;i++){
                        increment2();//a++采用Mutex进行同步处理
                    }
                    try {
                        barrier.await();//等30个线程累加完毕
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        barrier.await();
        System.out.println("加锁后,a="+a);
    }
    /**
     * 没有同步措施的a++
     * @return
     */
    public static void increment1(){
        a++;
    }
    /**
     * 使用自定义的Mutex进行同步处理的a++
     */
    public static void increment2(){
        mutex.lock();
        a++;
        mutex.unlock();
    }
}

TestMutex

在这里插入图片描述
这运行结果充分说明了i++不是原子操作,可能发生线程安全问题

源码详解

AQS详解_第10张图片
AQS详解_第11张图片
解释:为什么会存在争夺的情况,原因是因为不仅仅是有后继节点线程会去抢资源,还可能会有新产生的线程也会去抢资源,所以会产生争夺现象
AQS详解_第12张图片
提示:下面的源码可能看不懂,下图第2张图是源码的翻版

3.1 acquire(int)

AQS详解_第13张图片
AQS详解_第14张图片
AQS详解_第15张图片
小疑问:上述为什么会是被中断呢?

3.1.2 addWaiter(Node)

小疑问:下面addWaiter()方法里面还有enq()是入列(enqueue)的意思
AQS详解_第16张图片
AQS详解_第17张图片
AQS详解_第18张图片
——————————————————————————————————

3.1.2.1 enq(Node)

提示: 要注意下面的enq()方法体里面有一个for死循环,这个就是自旋,一直做for循环,直到CAS,执行成功return t为止
①因为存在争用,可能多个线程入队,所以CAS操作来保证入队的原子性
②tai节点又是volatile的,保证可见性

AQS详解_第19张图片

3.1.3 acquireQueued(Node, int)

在这里插入图片描述
AQS详解_第20张图片
**解释:**上述说的“如果自己可以休息”就是说本线程是否被请求中断不是自身控制的,是由外在对象改变的

到这里了,我们先不急着总结acquireQueued()的函数流程,先看看shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()具体干些什么。

3.1.3.1 shouldParkAfterFailedAcquire(Node, Node)

AQS详解_第21张图片
整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。

3.1.3.2 parkAndCheckInterrupt()

如果线程找好安全休息点后,那就可以安心去休息了。此方法就是让线程去休息,真正进入等待状态。
在这里插入图片描述
提示: 需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。

对acquireQueue()大总结:

AQS详解_第22张图片
AQS详解_第23张图片
AQS详解_第24张图片

3.2 release(int)

上一小节已经把acquire()说完了,这一小节就来讲讲它的反操作release()吧。此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock()。下面是release()的源码:
AQS详解_第25张图片
 逻辑并不复杂。它调用tryRelease()来释放资源。有一点需要注意的是,它是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计tryRelease()的时候要明确这一点!!

3.2.1 tryRelease(int)

下面是AQS的方法源码,需要我们去重写之
在这里插入图片描述
 跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。

3.2.2 unparkSuccessor(Node)

此方法用于唤醒等待队列中下一个线程。下面是源码:
小疑问:可以看到下面的for循环是从同步队列的尾部往前逐个遍历的,然后逐个往前遍历,直到找出最前的那个未放弃的线程(有效的)
AQS详解_第26张图片
这个函数并不复杂。一句话概括:用unpark()唤醒等待队列中最前边的那个未放弃线程,这里我们也用s来表示吧。此时,再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了!!And then, DO what you WANT!

3.2.3 小结
  release()是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。

共享模式下的方法

3.3 acquireShared(int)

此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。下面是acquireShared()的源码:
AQS详解_第27张图片

3.3.1 doAcquireShared(int)

此方法用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。下面是doAcquireShared()的源码:
AQS详解_第28张图片
在这里插入图片描述
解释: 从上面的这段话就可以看出,并发就比较低效,是要在代码中处理这个死局问题的

3.3.1.1 setHeadAndPropagate(Node, int)

AQS详解_第29张图片
AQS详解_第30张图片
小疑问: 可能这里不是很明白为什么要加上 h ==null和h.waitStatus<0?从上图可以看出p.next=null(解释: p是原头节点,p.next=null目的是为了回收原头节点)是在setHeadAndPropagate()函数之后执行的,说明在setHeadAndPropagate()函数体里面原头节点肯定不为null,那么为什么要在if()里面加上h ==null和h.waitStatus<0呢?

此方法在setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后继结点,毕竟是共享
doReleaseShared()我们留着下一小节的releaseShared()里来讲。
  AQS详解_第31张图片

3.4 releaseShared()

上一小节已经把acquireShared()说完了,这一小节就来讲讲它的反操作releaseShared()吧。此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。下面是releaseShared()的源码:
 AQS详解_第32张图片
 在这里插入图片描述
 小疑问: 我感觉这样做的思路岂不是互斥了吗

3.4.1 doReleaseShared()

此方法主要用于唤醒后继。下面是它的源码:

你可能感兴趣的:(并发编程系列,AQS详解)