千家信息网

AbstractQueuedSynchronizer预热的示例分析

发表于:2024-11-17 作者:千家信息网编辑
千家信息网最后更新 2024年11月17日,这篇文章给大家分享的是有关AbstractQueuedSynchronizer预热的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。核心方法预热 // 我不确定
千家信息网最后更新 2024年11月17日AbstractQueuedSynchronizer预热的示例分析

这篇文章给大家分享的是有关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预热的示例分析"这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!

0