前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java并发之AQS详解

Java并发之AQS详解

作者头像
用户5640963
发布2019-07-25 11:11:17
1K1
发布2019-07-25 11:11:17
举报
文章被收录于专栏:卯金刀GG卯金刀GG

AQS是AbstractQueuedSynchronizer的简称。AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。AQS为一系列同步器依赖于一个单独的原子变量(state)的同步器提供了一个非常有用的基础。子类们必须定义改变state变量的protected方法,这些方法定义了state是如何被获取或释放的。鉴于此,本类中的其他方法执行所有的排队和阻塞机制。子类也可以维护其他的state变量,但是为了保证同步,必须原子地操作这些变量。

介绍下FIFO的等待队列,这是一个双向队列,主要作用是用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果已经被占用,那么当前线程就会被构造成一个Node节点加入到同步队列的尾部,队列的头节点是成功获取锁的节点,当头节点线程释放锁时,会唤醒后面的节点并释放当前头节点的引用。

如图:

独占锁的获取和释放流程

获取锁

1、调用入口方法acquire(arg) 2、调用模版方法tryAcquire(arg)尝试获取锁,若成功则返回,若失败则走下一步 3、将当前线程构造成一个Node节点,并利用CAS将其加入到同步队列到尾部,然后该节点对应到线程进入自旋状态 4、自旋时,首先判断其前驱节点释放为头节点&是否成功获取同步状态,两个条件都成立,则将当前线程的节点设置为头节点,如果不是,则利用LockSupport.park(this)将当前线程挂起 ,等待被前驱节点唤醒

释放锁

1、调用入口方法release(arg)

2、调用模版方法tryRelease(arg)释放同步状态

3、获取当前节点的下一个节点

4、利用LockSupport.unpark(currentNode.next.thread)唤醒后继节点(接获取的第四步)

共享锁的获取和释放流程

获取锁

1、调用acquireShared(arg)入口方法

2、进入tryAcquireShared(arg)模版方法获取同步状态,如果返返回值>=0,则说明同步状态(state)有剩余,获取锁成功直接返回

3、如果tryAcquireShared(arg)返回值<0,说明获取同步状态失败,向队列尾部添加一个共享类型的Node节点,随即该节点进入自旋状态

4、自旋时,首先检查前驱节点释放为头节点&tryAcquireShared()是否>=0(即成功获取同步状态)

5、如果是,则说明当前节点可执行,同时把当前节点设置为头节点,并且唤醒所有后继节点

6、如果否,则利用LockSupport.unpark(this)挂起当前线程,等待被前驱节点唤醒

释放锁

1、调用releaseShared(arg)模版方法释放同步状态

2、如果释放成,则遍历整个队列,利用LockSupport.unpark(nextNode.thread)唤醒所有后继节点

独占锁和共享锁在实现上的区别

独占锁的同步状态值为1,即同一时刻只能有一个线程成功获取同步状态

共享锁的同步状态>1,取值由上层同步组件确定

独占锁队列中头节点运行完成后释放它的直接后继节点

共享锁队列中头节点运行完成后释放它后面的所有节点

共享锁中会出现多个线程(即同步队列中的节点)同时成功获取同步状态的情况

重入锁

重入锁指的是当前线成功获取锁后,如果再次访问该临界区,则不会对自己产生互斥行为。Java中对ReentrantLock和synchronized都是可重入锁,synchronized由jvm实现可重入即使,ReentrantLock都可重入性基于AQS实现。

同时,ReentrantLock还提供公平锁和非公平锁两种模式。

首先来看看ReentrantLock的构造方法,它的构造方法有两个,如下图所示:

QQ图片20140110194534
QQ图片20140110194534

很显然,对象中有一个属性叫sync,有两种不同的实现类,默认是“NonfairSync”来实现,而另一个“FairSync”它们都是排它锁的内部类,不论用那一个都能实现排它锁,只是内部可能有点原理上的区别。先以“NonfairSync”类为例,实现lock()方法。

QQ图片20140110194615
QQ图片20140110194615

lock()方法先通过CAS尝试将状态从0修改为1。若直接修改成功,前提条件自然是锁的状态为0,则直接将线程的OWNER修改为当前线程,这是一种理想情况,如果并发粒度设置适当也是一种乐观情况。 若上一个动作未成功,则会间接调用了acquire(1)来继续操作,这个acquire(int)方法就是在AbstractQueuedSynchronizer当中了。 首先看tryAcquire(arg)这里的调用(当然传入的参数是1),在默认的“NonfairSync”实现类中,会这样来实现:

QQ图片20140110194650
QQ图片20140110194650

○ 首先获取这个锁的状态,如果状态为0,则尝试设置状态为传入的参数(这里就是1),若设置成功就代表自己获取到了锁,返回true了。状态为0设置1的动作在外部就有做过一次,内部再一次做只是提升概率,而且这样的操作相对锁来讲不占开销。 ○ 如果状态不是0,则判定当前线程是否为排它锁的Owner,如果是Owner则尝试将状态增加acquires(也就是增加1),如果这个状态值越界,则会抛出异常提示,若没有越界,将状态设置进去后返回true(实现了类似于偏向的功能,可重入,但是无需进一步征用)。 ○ 如果状态不是0,且自身不是owner,则返回false。

对tryAcquire()的调用判定中是通过if(!tryAcquire())作为第1个条件的,如果返回true,则判定就不会成立了,自然后面的acquireQueued动作就不会再执行了,如果发生这样的情况是最理想的。 无论多么乐观,征用是必然存在的,如果征用存在则owner自然不会是自己,tryAcquire()方法会返回false,接着就会再调用方法:acquireQueued(addWaiter(Node.EXCLUSIVE), arg)做相关的操作。 这个方法的调用的代码更不好懂,需要从里往外看,这里的Node.EXCLUSIVE是节点的类型,看名称应该清楚是排它类型的意思。接着调用addWaiter()来增加一个排它锁类型的节点,这个addWaiter()的代码是这样写的:

代码语言:javascript
复制
/**
     * 把Node节点添加到同步队列的尾部
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);  // 以独占模式把当前线程封装成一个Node节点
        // 尝试快速入队
        Node pred = tail;  // 当前队列的尾节点赋给pred
        if (pred != null) {  // 先觉条件 尾节点不为空
            node.prev = pred;  // 把pred作为node的前继节点
            if (compareAndSetTail(pred, node)) { //利用CAS把node作为尾节点
                pred.next = node;    // 把node作为pred的后继节点
                return node;       // 直接返回node
            }
        }
        enq(node);  // 尾节点为空或者利用CAS把node设为尾节点失败
        return node;
    }
    /**
     * 采用自旋的方式把node插入到队列中
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // 如果t为空,说明队列为空,必须初始化
                if (compareAndSetHead(new Node())) // 新建一个节点利用CAS设为头节点,就是这样的形式 head=tail=null
                    tail = head;
            } else {    // 尾节点不为空的情况
                node.prev = t;  // 把t设为node的前驱节点
                if (compareAndSetTail(t, node)) {  // 利用CAS把node节点设为尾节点
                    t.next = node;   // 更改指针  把node作为t的后继节点
                    return t;   // 直接返回t
                }
            }
        }
    }

这里创建了一个Node的对象,将当前线程和传入的Node.EXCLUSIVE传入,也就是说Node节点理论上包含了这两项信息。代码中的tail是AQS的一个属性,刚开始的时候肯定是为null,也就是不会进入第一层if判定的区域,而直接会进入enq(node)的代码,那么直接来看看enq(node)的代码。

看到了tail就应该猜到了AQS是链表吧,没错,而且它还应该有一个head引用来指向链表的头节点,AQS在初始化的时候head、tail都是null,在运行时来回移动。此时,我们最少至少知道AQS是一个基于状态(state)的链表管理方式。

这段代码就是链表的操作。 首先这个是一个死循环,而且本身没有锁,因此可以有多个线程进来,假如某个线程进入方法,此时head、tail都是null,自然会进入if(t == null)所在的代码区域,这部分代码会创建一个Node出来名字叫h,这个Node没有像开始那样给予类型和线程,很明显是一个空的Node对象,而传入的Node对象首先被它的next引用所指向,此时传入的node和某一个线程创建的h对象如下图所示。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档