侧边栏壁纸
博主头像
一定会去到彩虹海的麦当

说什么呢?约定好的事就一定要做到啊!

  • 累计撰写 63 篇文章
  • 累计创建 16 个标签
  • 累计收到 3 条评论

目 录CONTENT

文章目录

【并发进阶】——AQS原理

一定会去到彩虹海的麦当
2022-02-02 / 0 评论 / 0 点赞 / 133 阅读 / 4,853 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-02-11,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

🤝非常感谢大家的支持与点赞👍

概述

全称是 AbstractQueuedSynchronizer(队列同步器),是阻塞式锁和相关的同步器工具的框架

特点:

  1. 用 state 属性来表示同步状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁

独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源

  1. 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryLis
  2. 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet

同步器的接口

同步器提供了3个方法来来访问或修改同步状态:

  • getState - 获取 state 状态

  • setState - 设置 state 状态

  • compareAndSetState - cas 机制设置 state 状态

子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)

  • tryAcquire

    独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态

  • tryRelease

    独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态

  • tryAcquireShared

    共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败

  • tryReleaseShared

    共享式释放同步状态

  • isHeldExclusively

    当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占

release方法跟tryRelease方法区别在于release方法释放后会唤醒阻塞线程,而tryRelease并不会

设计原理

基本思想

1、获取锁的逻辑

while(state 状态不允许获取) {
 if(队列中还没有此线程) {
 入队并阻塞
 }
}
当前线程出队

2、释放锁的逻辑

if(state 状态允许了) {
 恢复阻塞的线程(s)
}

要点

  • 原子维护 state 状态
  • 阻塞及恢复线程
  • 维护队列

state设计

  • state 使用 volatile 配合 cas 保证其修改时的原子性
  • state 使用了 32bit int 来维护同步状态,因为当时使用 long 在很多平台下测试的结果并不理想

阻塞恢复设计

  • 早期的控制线程暂停和恢复的 api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的 resume 那么 suspend 将感知不到
  • 解决方法是使用 park & unpark 来实现线程的暂停和恢复,先 unpark 再 park 也没 问题
  • park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细 park 线程还可以通过 interrupt 打断

队列设计

同步队列

  • 使用了 FIFO 先入先出队列,并不支持优先级队列
  • 同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再 次尝试获取同步状态。

image-20220105234924565

队列中有 head 和 tail 两个指针节点,都用 volatile 修饰配合 cas 使用,每个节点有 state 维护节点状态

伪代码分析

1、入队伪代码,只需要考虑 tail 赋值的原子性

do {
 // 原来的 tail
 Node prev = tail;
 // 用 cas 在原来 tail 的基础上改为 node
} while(tail.compareAndSet(prev, node))

image-20220105235232694

2、出队伪代码

// prev 是上一个节点
while((Node prev=node.prev).state != 唤醒状态) {
}
// 设置头节点
head = node;

image-20220105235242176

由于只有一个线程能 够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证

如何保证节点的正确添加?

多线程情况很有可能出现获取同步状态失败而并发添加到同步队列的情况,我们如何保证节点可以正确被添加呢

 private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            // 队列中还没有元素 tail 为 null
            if (t == null) {
                // 将 head 从 null -> dummy
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 将 node 的 prev 设置为原来的 tail
                node.prev = t;
                // 将 tail 从原来的 tail 设置为 node
                if (compareAndSetTail(t, node)) {
                    // 原来 tail 的 next 设置为 node
                    t.next = node;
                    return t;
                }
            }
        }
    }

在enq(final Node node)方法中,同步器通过“死循环”来保证节点的正确添加,在“死循 环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线 程不断地尝试设置。可以看出,enq(final Node node)方法将并发添加节点的请求通过CAS变 得“串行化”了。

节点的自旋

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                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);
        }
    }

节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自旋地观察自己的前驱是否是头节点,如果是则尝试获取同步状态。

自旋过程中,节点和节点之间在循环检查 的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点,这样就使得节点的释 放规则符合FIFO,并且也便于对过早通知的处理。

image-20220106063350804

整体流程

1、线程在获取锁失败后会被封装成节点放入同步队列的队尾,需要CAS设置

2、自旋判断前驱节点是不是头结点,如果是获取同步状态,否则进入阻塞状态直到被头结点唤醒

3、获取成功,将持有同步状态的线程所在的节点设置为头节点

4、头节点线程完成任务后,释放了同步状态,然后会唤醒它的后继节点,同时后继节点的线程被唤醒后还需要检查自己的前驱节点是不是头节点。

5、这样队列就会以逐个往下的方式依次获得同步状态,执行任务,符合先入先出。

值得注意的是这里的头结点会被设置为null

头节点是不参与排队的,因为它已经获得了同步状态了,那么就说明该头节点的相关线程已经在执行相应的业务逻辑了,而在执行完业务逻辑,释放同步状态后,该头节点是肯定要被垃圾回收的,防止内存空间的浪费,这里就涉及到了gc root,如果对象还有引用的话,垃圾回收器是不会回收它的,所以需要把头节点持有的各种引用都置为null,方便之后的垃圾回收(注意头节点置为null只是取消了与线程的引用,并不是将需要执行的线程置为null)

自定义同步器

代码实现

1、首先我们先自定义锁MyLock(不可重入锁),然后再在锁中再定义内部类同步器MySnc

class MyLock implements Lock {
	class MySync extends AbstractQueuedSynchronizer{
	......
	}

.....
}

2、我们先实现同步器MySync的方法

 class MySync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            if(compareAndSetState(0, 1)) {
                // 加上了锁,并设置 owner 为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        @Override // 是否持有独占锁
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        public Condition newCondition() {
            return new ConditionObject();
        }
    }`

3、再利用同步器,实现锁的方法

// 自定义锁(不可重入锁)
class MyLock implements Lock {

    // 独占锁  同步器类
    class MySync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            if(compareAndSetState(0, 1)) {
                // 加上了锁,并设置 owner 为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        @Override // 是否持有独占锁
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        public Condition newCondition() {
            return new ConditionObject();
        }
    }

    private MySync sync = new MySync();

    @Override // 加锁(不成功会进入等待队列)
    public void lock() {
        sync.acquire(1);
    }

    @Override // 加锁,可打断
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override // 尝试加锁(一次)
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override // 尝试加锁,带超时
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override // 解锁
    public void unlock() {
        sync.release(1);
    }

    @Override // 创建条件变量
    public Condition newCondition() {
        return sync.newCondition();
    }
}

通过以上实现我们可以认识到:

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的 方法,随后将同步器(MySync)组合在自定义同步组件(MyLock)的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法

同步器与锁的关系:

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。

可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者, 它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。

0

评论区