AbstractQueuedSynchronizer之AQS

在进行分析AbstractQueuedSynchronizer之前必须得了解 LockSupport ;篇幅较长,请耐心看下去,一定会有所收获。

1. LockSupport

1.1 是什么?

LockSupport是用来创建锁和其它同步类的基本线程阻塞原语。

LockSupport中的park()和unpark的作用分别是阻塞线程解除阻塞线程

1.2 主要方法

1.2.1 阻塞:park()/park(Object blocker) : 阻塞当前线程/阻塞传入的具体线程

permit默认时0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回。

 

1.2.2 唤醒:unpark(Thread thread):  唤醒处于阻塞状态的指定线程

 

调用unpark(thread)方法后,就会将thread线程的许可证permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程即之前阻塞中的LockSupport.park() 方法会立即返回。

 

1.2 线程等待唤醒机制

1.2.1 三种让线程等待和唤醒的方法

1.2.1.1 使用Object的wait()方法让线程等待,使用Object的notify()方法唤醒线程

private static Object objectLock = new Object();
    public static void synchronizedNotify() throws InterruptedException {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " -> come in ...");
            synchronized (objectLock) {
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " 被唤醒了....");
        },"A").start();

        // 休息一分钟
        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " 通知唤醒线程....");
            synchronized (objectLock) {
              objectLock.notify();
            }

        },"B").start();
    }

 

此时将顺序颠倒---> 先让B线程唤醒 ,给A线程加个睡眠时间 

 private static Object objectLock = new Object();
    public static void synchronizedNotify() throws InterruptedException {
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " -> come in ...");
            synchronized (objectLock) {
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " 被唤醒了....");
        },"A").start();

        // 休息一分钟
        //TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " 通知唤醒线程....");
            synchronized (objectLock) {
              objectLock.notify();
            }

        },"B").start();
    }

此时无法唤醒A线程!!!

 

1.2.1.2 使用JUC中Condition的await方法让线程等待,使用signal()方法唤醒线程

private static Lock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();
    private static void lockCondition() throws InterruptedException {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " -> come in ...");
            lock.lock();
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }

            System.out.println(Thread.currentThread().getName() + " 被唤醒了....");
        },"A").start();

        // 休息一分钟
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " 通知唤醒线程....");
            lock.lock();
            try {
                condition.signal();
            }finally {
                lock.unlock();
            }

        },"B").start();
    }

此时将顺序颠倒---> 先让B线程唤醒 ,给A线程加个睡眠时间  ;则线程A一直等待唤醒操作...与1类似情况出现;

1.2.1.3 LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

private static void lockSupportPark() throws InterruptedException {
        Thread a = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "->"+System.currentTimeMillis()+ " -> come in ...");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "->"+System.currentTimeMillis()+ " 被唤醒了....");
        }, "A");
        a.start();

        // 休息一分钟
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            System.out.println(Thread.currentThread().getName() +"->"+System.currentTimeMillis()+ " 通知唤醒线程....");
                LockSupport.unpark(a);
        },"B").start();
    }

 此时将顺序颠倒---> 先让B线程唤醒 ,给A线程加个睡眠时间 

 private static void lockSupportPark() throws InterruptedException {
        Thread a = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "->"+System.currentTimeMillis()+ " -> come in ...");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "->"+System.currentTimeMillis()+ " 被唤醒了....");
        }, "A");
        a.start();

        // 休息一分钟
        //TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            System.out.println(Thread.currentThread().getName() +"->"+System.currentTimeMillis()+ " 通知唤醒线程....");
                LockSupport.unpark(a);
        },"B").start();
    }

综上所述: LockSupport 可以解决 先唤醒 后等待的问题。

 

1.3 LockSupport是用来创建锁和其他同步类的基本线程阻塞源语

LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。

LocakSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程。

LocakSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0开关,默认时0.

调用一次unpark就加1变成1

调用一次park会消费permit,也就是将1变成0,同时park立即返回。

如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变成1),这时调用unpark会把permit置为1。

每个线程都有一个相关的permit,permit对多只有一个,重复调用unpark也不会积累凭证。
 

形象的理解

线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。

当调用park方法时

        * 如果有凭证,则会直接消耗掉这个凭证然后正常退出;

         * 如果无凭证,就必须阻塞等待凭证可用;

而unpark则相反,他会增加一个凭证,但凭证最多只能有一个,累加无效。

1.4 为什么可以先唤醒线程后阻塞线程?

因为unpark获得一次凭证后,之后调用park方法,就可以正常的凭证消费,故不会阻塞。

1.5 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

因为凭证的数量最多为1,连续两次唤醒不会叠加permit,只会增加一个凭证;而调用两次park则需要消费两个凭证,证不够,不能放行。

以上就是LockSupport相关的内容,下面开始打AQS: 

2. AbstractQueuedSynchronizer

2.1 是什么?

抽象队列同步器】是用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态

 

2.2 AQS为什么是JUC内容中的最重要的基石

2.2.1 进一步理解锁和同步器的关系

锁: 面向锁的使用者 

定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可。

同步器: 面向锁的实现者

比如java并发大神DougLee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理,阻塞线程排队和通知唤醒机制等。

2.2.2 和AQS相关

ReentrantLock:

 ReentrantReadWriteLock

 CountDownLatch

 Semaphore

 等等。。。

2.3 能干嘛

加锁会导致阻塞

有阻塞就需要排队,实现排队必然需要某种形式的队列来进行管理

抢到资源的线程直接使用处理业务逻辑,抢不到资源的必然涉及一种排队等待机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的客户只能去候客区排队等待),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去窗口办理业务)

既然有排队等待机制那么就一定会有某种队列形成,这样的队列是什么数据结构呢?

如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现,将暂时获取不到锁的线程加入队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node),通过CAS、自旋以及LockSupport.park()方式,维护state变量的状态,使并发达到同步的控制效果。

官网解释:

image

 

AQS使用一个volatile的int类型成员变量【State:0表示无线程占用,1表示占用来表示同步状态,通过内置的FIFO队列(CLH队列变种-->双端队列)来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改

image

 

2.4 AQS组成架构

2.4.5 AQS自身

2.4.5.1 AQS的int变量[volatile修饰]
AQS的同步状态state成员变量 银行办理业务的受理窗口状态
* 0就是没人,自由状态可以办理
* 大于等于1,有人占用窗口,等着去

2.4.5.2 AQS的CLH队列 
CLH队列(三个大牛的名字组成)为一个双向队列

 2.4.6 内部类Node(Node类在AQS类的内部)

        队列示意图:

 

2.4.6.1 Node的int变量 
Node的等待状态 waitStatus成员变量 ---> volatile int waitStatus
​等待区其他线程的等待状态,队列中每个排队的个体就是一个Node

2.4.6.2 节点等待模式

标记节点等待模式的标记,类型也是 Node,
SHARED 值不为 null 时,表示是共享模式;

EXCLUSIVE 不为 null 时,表示是独占模式:

/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;

2.4.6.3 节点等待状态

waitStatus,用于表示线程已取消:

 /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;

       
        volatile int waitStatus;
属性值状态含义
waitStatusINITIAL--> 0初始状态
SIGNAL--> -1后继的节点处于等待状态,当前节点的线程如果释放了同步状态或者被取消(当前节点状态置为-1),将会通知后继节点,使后继节点的线程得以运行;
CONDITION--> -2节点处于等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点从等待队列中转移到同步队列中,加入到对同步状态的获取中
-3表示下一次的共享状态会被无条件的传播下去
CANCELLED-->1当该线程等待超时或者被中断,需要从同步队列中取消等待,则该线程被置1,即被取消(这里该线程在取消之前是等待状态)。节点进入了取消状态则不再变化;

2.4.6.4 Node节点详解

image

 

Node nextWaiter:等待节点的后继节点。如果当前节点是共享的,那么这个字段是一个SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点共用一个字段。(注:比如说当前节点A是共享的,那么它的这个字段是shared,也就是说在这个等待队列中,A节点的后继节点也是shared。如果A节点不是共享的,那么它的nextWaiter就不是一个SHARED常量,即是独占的

 

2.5 从ReentrantLock解读AQS源码

Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的。 

2.5.1 公平锁和非公平的区别

 

 

 

可以明显看出公平锁和非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态多了一个限制条件:hasQueuePredecessors() 这是公平锁加锁时判断等待队列中是否存在有效节点的方法;主要是用来判断线程需不需要排队,因为队列是FIFO的,所以需要判断队列中有没有相关线程的节点已经在排队了。有则返回true表示线程需要排队,没有则返回false则表示线程无需排队。

 2.5.2 lock()方法   --> 从非公平锁解析

 

对比公平锁和非公平锁的tryAcquire()方法的实现代码,其实差别就在于非公平锁获取锁时比公平锁少了一个判断 !hasQueuedPredecessors(). ​

判断了是否需要排队,导致公平锁和非公平锁的差异如下:

公平锁: 公平锁讲究先来后到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;

非公平锁: 不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark(), 之后还是需要竞争锁(存在线程竞争的情况下)

image

小总结: 

线程会尝试利用CAS去判断state是不是0,是则更新;第二个线程肯定是失败的,因为第一个线程已经将state设置为1了,第二个线程及后续线程则会执行acquire(1)方法。

  2.5.3 acquire(1)方法 

此方法在第二个及后续线程未抢占到资源而执行;

主要分为三大流程:

1. tryAcquire(arg) : 再次尝试获取锁;

2. addWaiter(Node.EXCLUSIVE)  : 将线程封装成Node节点,塞入等待队列。

3. acquireQueued(addWaiter(Node.EXCLUSIVE), arg): 再次尝试获取锁,真正意义上的进入等待队列排队;

2.5.4 tryAcquire(arg)

非公平锁实现:nonfairTryAcquire(int acquires)

image

小总结

1.  第二个线程来到当前方法,先获取AQS的状态state[volatile修饰,保证线程可见],判断是否持有锁;

2. 如果没有持有锁,则通过CAS方式取抢占资源,如果抢占更新了state,并将排它线程设置为自身;返回true

3. 如果当前线程等于排它线程,即某个线程可以多次调用同一个ReentrantLock可重入锁,每调用一次给state+1,由于某个线程已经持有锁了,所以不会竞争,因此不需要CAS设置state(相当于一个偏向锁);返回true

4. 获取锁失败,继续 addWaiter()操作

2.5.5 addWaiter(Node.EXCLUSIVE) 

addWaiter(Node mode)

image

 enq(Node node)

image

双向链表中,第一个节点为虚节点(也叫哨兵节点)  ,其实并不存储任何信息,只是占位。真正的第一个有数据的节点从从第二个节点开始的。

小总结:

1. 先创建一个当前线程节点Node,模式为独占;

2. 判断队列尾结点是否为空 (第二个线程肯定为null,则走enq(node))

        2.1 不为空: 则将node的前指针指向尾结点;

        2.2 通过CAS的方式更新尾结点;

        2.3 再将此尾结点的后指针指向 node;

3.  enq(node): 初始化队列(自旋的方式)

        3.1  第一次进来---> 尾结点为空,通过CAS初始化头结点,并头结点等于尾结点的引用。此次节点为哨兵节点,即等待线程为null;

        3.2  第二次进来--->  将node节点前指针指向哨兵节点,再通过CAS的方式设置node节点为尾结点,并将哨兵节点的后指针指向node;

2.5.5 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

再次抢占锁资源,抢失败则会进入 shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()

 

 

小总结:

1. 加入队列后的node节点会再次尝试抢占锁资源,如果成功则将该node节点出队;失败则进入shouldParkAfterFailedAcquire(p, node)

        出队: 将当前node节点设置头结点,并将线程设置为null,node的前指针指向null;哨兵节点的后指针指向null;

2.  先获取前驱节点的状态[waitStatus] ;如果前驱节点的waitStatus是SIGNAL状态,即shouldParkAfterFailedAcquire方法会返回true,程序会继续向下执行parkAndCheckInterrupt方法,用于将当前线程挂起。

image

 

        2.1 如果是SIGNAL状态,即等待被占用的资源释放,直接返回true;准备继续调用parkAndCheckInterrup()方法;

        2.2 状态大于0 说明是CANCELLED状态,会循环判断前驱节点的前驱节点也为CANCELLED状态,忽略该状态的节点,重新连接队列;

        2.3 通过CAS的方式将当前节点的前驱节点设置为SIGNAL状态,用于后续唤醒操作,程序第一次执行到这返回为false,还会进行外层第二层循环。

3. parkAndCheckInterrupt(): 线程挂起,程序不会进行向下执行。

image

 

2.5.6 unlock

在2.5.5中可知,当前线程已经被挂起进入等待;此时是怎么唤醒的呢?此时开打unlock

2.5.6.1 sync.release(1)   

2.5.6.2 tryRelease(arg)

2.5.6.3 unparkSuccessor

 

小总结:

1. unlock调用AQS实现类release;尝试释放锁,即获取当前AQS的锁状态并减一,将排它线程设置为null,当前状态更新为0;返回true;

2. 头结点不为空,且头结点的等待状态不为0 ,则通过unparkSuccessor唤醒阻塞线程;

3.  更新头结点的等待状态为0;如果头结点的下一个节点不为空,则调用unpark 唤醒该节点线程;

4. 唤醒该node节点后,则会通过自旋的方式尝试获取锁,获取锁之后将该节点设置为哨兵节点;

综上就是ReentrantLock 非公平锁中AQS的实现全过程.... 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值