AbstractQueuedSynchronizer预热的示例分析

发布时间:2021-09-23 14:35:54 作者:小新
来源:亿速云 阅读:110

这篇文章给大家分享的是有关AbstractQueuedSynchronizer预热的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。

核心方法预热

    // 我不确定有多少人卡在这里
    // 我是这么理解的 某个对象在jvm当中 是用一块数据来描述对象的所有信息
    // 那么问题来了 如果我要设置某个对象的字段 通常的方法 对象引用.setXXXField(xxx)这个是通常的方法
    // 还有一种比较特别的 unsafe提供的 unsafe.objectFieldOffset获取某个字段的偏移量 可以理解为存储信息的地址
    // 获得了偏移地址之后 就可以使用 unsafe.compareAndSwapObject来原子的设置某个对象的字段
    // 就是说 绕过通用的流程 直接修改相关数据了 顺带而且是原子性的
    // 可以理解为玩游戏用外挂直接修改内存这种场景
    headOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
    unsafe.compareAndSwapObject(this, headOffset, expect, update);
    
    tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
    unsafe.compareAndSwapObject(this, tailOffset, expect, update);

    /**
     * 独占式获取同步状态,忽略线程的打断。
     * 获取同步状态的逻辑是由重写的模板方法tryAcquire来实现的。
     * 如果获取同步状态成功,则方法就直接返回。
     * 否则,线程就会入队,一直会处于阻塞或者自旋,直到重复尝试tryAcquire成功。
     * 该方法就是接口Lock#lock的实现。
     * (从方法的介绍上面理解,就是说,这个接口直接的效果就是,获取同步成功,线程就从这个方法继续执行下去,如果不成功;
     * 那么内部会经过一系列复杂的逻辑计算,直接体现就是线程不会继续执行下去,就一直处于这个方法内部。不执行下去的原因是:线程可能处于自旋或者阻塞。)
     * @param arg 同步状态参数  透传进tryAcquire并且不响应终端或者其他情况(超时)
     * 
     * 由两种判断逻辑
     * 1. tryAcquire(arg) -> 返回
     * 2. tryAcquire(arg) -> addWaiter(Node.EXECLUSIVE) -> acquireQueued(lastValue, arg) -> 返回并且可能会中断线程
     * 
     * addWaiter(Node node) 入队
     * acquireQueued(final Node node, int arg) 自旋或者阻塞
     * 
     * 这个方法就是把整个流程已经写死了,必定会经过这么几个步骤。
     * 唯一可以影响该方法中的流程,只能是模板方法tryAcquire,它的返回与否,导致流程的走向。
     * 把自旋或者阻塞安排在if的条件语句中 会令人初步一看会感觉非常难受。(大神可以这么用,我们平时还是少用)。
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    // 老老实实的我,一般会这么写 见笑见笑
    public final void acquire(int arg) {
        // 尝试获取同步状态
        if (tryAcquire(arg)) {
            return;
        } else {
            // 先入队 这里会有一个死循环
            Node newNode = addWaiter(Node.EXCLUSIVE);
            // 再自旋获取同步状态 或者阻塞 这里也会有死循环
            boolean shouldCurrentThreadInterrupted = acquireQueued(newNode, arg);
            // 再判断是否需要线程中断
            if (shouldCurrentThreadInterrupted) {
                selfInterrupt();
            }
        }
    }

    // 接下来看看这个模板方法的介绍
    /**
     * 尝试独占式获取同步状态。
     * 该方法需要查询对象当前状态,判断同步状态是否符合预期。
     * (我的理解就是,需要自己实现自己的逻辑,判断自己所要实现的逻辑是否符合自己的预期。记住是独占模式)
     *
     * 该方法经常再线程执行同步时被调用。
     * 如果方法返回失败,那么线程就应该入队了,即使线程还没做好入队准备。
     * (这里的意思就是说,线程在竞争锁之前,最好做好充足的准备工作,也就是前置逻辑要执行完,比如各种初始化判断。加锁之后就应该是确确实实的逻辑操作了,最好不要加完锁之后,又去判断各种前置业务逻辑操作。这个就是我理解的大师所要阐述的最佳实践。)
     * 入队的线程只能等待别人释放之后唤醒。
     * 一般前置方法就是为了实现Lock#tryLock这个。
     *
     * 默认实现式UnsupportedOperationException异常。
     *
     * @param arg 请求参数。
     *        一般这个值是方法唯一的参数,或者保存于条件等待中。
     *        所以不建议为这个值赋予更多其他含义。
     *        (我认为这里的意思是,这个值不要和业务中的某个条件或者流程挂钩,让值单纯的标识同步状态就好了。)
     *
     * @return true加锁成功。
     * @throws IllegalMonitorStateException 如果获取同步时发现同步器处于一个不正确的状态时,
     *         那么就必须抛出这个异常,目的时为了同步器逻辑正确。
     *         (我的理解,同步器状态很重要,必须严肃对待,因为一旦某个过程状态不正确,后续的业务逻辑可能会发生各种不可知的结果,并且,debug起来非常麻烦,因为业务逻辑可能正确,原因是同步状态的出错。这种是很隐晦的。也就是说,一旦碰到IllegalMonitorStateException,个人认为最好中断运行,排错。即使开发者认为这个错误不重要。你都已经自己实现锁的逻辑了,任何一点小的逻辑失误,都会造成不可预估的结果。千里之堤毁于蚁穴啊。)
     * @throws UnsupportedOperationException 如果独占模式不支持抛异常
     */
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

   // 入队操作
   /**
     * 创建队列,并且把当前线程包装一下,指定某个节点模式,入队。
     *
     * @param mode Node.EXCLUSIVE 独占, Node.SHARED 共享
     * @return 新的节点
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 先尝试直接队尾添加 如果不行在进行完整的入队操作 Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        // 队尾有两种情况
        // 1 null 表示队列还没有初始化 初始化在enq(node)中
        // 2 != null 表示队列初始化了 那么尝试快速添加队尾这个操作 我认为就是优化操作了
        // (老老实实的我,一般并不会这么写,因为我比较稳妥。)
        // (其实优化操作,理论上来说,可以不用的。)
        // compareAndSetTail()这个原子性的操作 防止并发
        // 并发操作的特点就是,随时随地都可能发生几个线程同时执行,所以,并发点,尽量条件简单点,如果业务条件够复杂,一定要拆,而且要分优先级的。不然,动态变化的条件加上锁,噩梦。
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                // 入队操作只需要建立一个尾链接就可以
                pred.next = node;
                return node; // 注意 这里返回的是新的节点
            }
        }
        enq(node); // 这里方法返回的是节点前置的节点 但是没有使用 在唤醒流程中会复用这个方法
        return node;
    }

    // 完整的入队流程逻辑
    /**
     * 入队操作,一定要先初始化队列。
     * (死循环确保一定会入队成功,我对死循环的理解是,单线程不要用死循环,多线程可以适量的用,主线程不要用,非要用时情愿开个线程计算,等它计算结束再拿那个结果也可以。总结起来,能不用就不用,即使要用,千万别忘记了,自己在干什么。建议在自己精力最旺盛的时候,写带有死循环的逻辑。)
     * @param node 入队节点
     * @return 返回前置节点
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // 队列初始化
                // 原子性的设置头 这里注意这个head节点 这个head指向的node是一个空的node,里面没有node的关键数据的
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 双向队列 尝试把当前节点的头设置为原本队尾那个 只要下面的cas队列设置好那就操作成功 不行再循环再来
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

    /**
     * 设置队列首节点 (因为是双向,队首的前驱是null,这个null是为了释放节点的。)
     * 该方法仅仅只被同步器获取。
     * null的目的是为了GC也为了不必要的信号释放遍历。
     *
     * @param node 设置队首
     */
    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

    // 自旋
    /**
     * 独占不响应中断模式的线程获取同步方法。
     * 条件等待也使用该方法。
     *
     * @param node 节点
     * @param arg 获取同步参数
     * @return true 如果等待时线程被打断
     */
    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);
        }
    }


    /**
     * 当一个节点获取同状态失败时,检查并且更新它的状态。
     * 返回true,那么线程需要被阻塞。
     * 在所有的获取同步循环中,这个是最重要的信号控制。
     * 前置条件是前置节点确切的是节点的前置节点。
     *
     * @param pred 带有状态的前驱节点
     * @param node 节点
     * @return true 线程被阻塞
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * 前驱节点已经处于等待其他线程释放同步状态而将它唤醒。
             * 那么当前节点应该能够安全的被阻塞。
             */
            return true;

        if (ws > 0) {
            /*
             * 前驱节点已经是取消状态。
             * 跳过前驱节点在尝试。
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * 等待状态必须是0或者是传播状态(-3)。
             * 仅需要一个信号,而并不需要阻塞。(应该是共享模式下的逻辑。)
             * 调用者需要重新确保当前线程在阻塞之前是否需要获取同步状态。
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

    /**
     * 阻塞当前线程。恢复后检测线程是否被中断了。
     *
     * @return true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

感谢各位的阅读!关于“AbstractQueuedSynchronizer预热的示例分析”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!

推荐阅读:
  1. (爬虫预热)01 Requests模块
  2. 深入理解AbstractQueuedSynchronizer(AQS)

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

abstractqueuedsynchronizer

上一篇:windows电脑里无线网络连接上但上不了网的原因和解决方法是怎样的

下一篇:SpringBoot如何使用thymeleaf模板

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》