V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
yangyuhan12138
V2EX  ›  Java

了解 AQS 的进来讨论一下

  •  
  •   yangyuhan12138 · 2020-02-11 21:09:34 +08:00 · 2687 次点击
    这是一个创建于 1540 天前的主题,其中的信息可能已经有所发展或是发生改变。

    LinkedBlockingQueue 的 poll(long timeout, TimeUnit unit)方法是否会超过指定时间返回 比如 linkedBlockingQueue.poll(1,TimeUnit.SECONDS);

    我想要的结果是最多 1 秒钟没取到我就不取了 但是实际执行的结果可能是线程在这里会阻塞 1 秒多? 因为 java.util.concurrent.locks.Condition#awaitNanos 的实现方式如此

    23 条回复    2020-02-13 20:57:51 +08:00
    ppyybb
        1
    ppyybb  
       2020-02-11 21:55:37 +08:00 via iPhone
    会超过,但是 nanosecond 的精度很高了,差异很小,这就是为什么这个 api 要用纳秒做参数的原因,实际中应该可以忽略误差。
    optional
        2
    optional  
       2020-02-11 21:58:12 +08:00
    这个误差是可接受的,换个角度说,因为 STW 的存在,对时间很敏感的根本就不应该用带 GC 的语言。
    yangyuhan12138
        3
    yangyuhan12138  
    OP
       2020-02-11 22:31:52 +08:00
    @ppyybb 但是极端情况下:比如有多个线程在 poll,他们都加入了 AQS 等待队列,这个时候 awaitNanos 超时时间到了再把 awaitNanos 这个线程从 Condition 里的链表转移到 AQS 里的链表,由于先进先出原则.调用 awaitNanos 的这个线程得等到 AQS 之前的所有 Node 都出队之后才会到他去抢锁,所以这个值也可能很大甚至误差几秒钟都可能
    yangyuhan12138
        4
    yangyuhan12138  
    OP
       2020-02-11 22:33:14 +08:00
    @optional 可能也有点关系吧
    ppyybb
        5
    ppyybb  
       2020-02-11 23:34:57 +08:00 via iPhone
    @yangyuhan12138 你这个几秒完全没有根据啊,你前面得有多少 node 才会有这么大误差?我觉得完全没有必要考虑这种不切实际的情况
    mreasonyang
        6
    mreasonyang  
       2020-02-12 03:27:14 +08:00 via iPhone
    会超,但除非系统挂了,不然不可能超几秒,如果能模拟这种情况可以贴下代码讨论下
    lu5je0
        7
    lu5je0  
       2020-02-12 13:00:36 +08:00
    @yangyuhan12138 而且 LinkedBlockingDeque 中使用的是非公平的 ReentrantLock 锁,不会等到所有 node 都出队之后才去抢锁。
    yangyuhan12138
        8
    yangyuhan12138  
    OP
       2020-02-13 02:17:46 +08:00
    ExecutorService get = Executors.newCachedThreadPool();
    LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue();
    for (int i = 0; i < 100000; i++) {
    get.submit(() -> {
    try {
    System.out.println("成功获取"+linkedBlockingQueue.take());
    Thread.sleep(10000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    });
    }
    get.submit(() -> {
    long l = System.currentTimeMillis();
    Object poll = null;
    try {
    poll = linkedBlockingQueue.poll(1000, TimeUnit.MILLISECONDS);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(poll);
    System.out.println( System.currentTimeMillis()-l);

    });

    @mreasonyang
    @ppyybb
    @lu5je0
    @optional
    yangyuhan12138
        9
    yangyuhan12138  
    OP
       2020-02-13 02:21:23 +08:00
    @mreasonyang
    @ppyybb
    @lu5je0
    @optional
    大致代码如上 理论上线程数量越多 AQS 里的链表越长,时间越长,但是正如 @lu5je0 所说的,AQS 默认实现为非公平锁,有可能一来就直接拿到锁而不进 AQS 链表,所以结果为 1s,但是如果他进了 AQS 链表就会产生误差,我电脑只能跑到 10w 线程 如图所示
    yangyuhan12138
        10
    yangyuhan12138  
    OP
       2020-02-13 02:25:47 +08:00
    yangyuhan12138
        11
    yangyuhan12138  
    OP
       2020-02-13 02:29:40 +08:00
    淦 为啥我的图不显示
    ppyybb
        12
    ppyybb  
       2020-02-13 12:27:10 +08:00 via iPhone
    @yangyuhan12138 我在 mac 上试了下,由于 mac 线程数量的限制,我只能用 4000 个线程去试,没有能复现你说的现象。

    另外我又看了下 aqs 的代码,
    take 应该是一直放在 condition 队列里面,因为没有被唤醒或者超时,不会进入 sync。
    poll ( 1000 )则应该是一开始进入 condition,超时后进入 sync 队列。

    那么在 sync 里面其实就只有一个元素,不会有很多 node 去争抢锁。而 condition queue 则和队列大小没有明显关系,超时是通过 park ( timeout )+自旋来实现的,和其他节点无关。

    有没有可能是你上面 submit 的循环在后台线程实际没有执行完,所以下面的 poll 等的时候实际上还有很多线程不停创建,由于线程太多,os 调度的时候不停切换,很多没有执行到 sleep 哪一步,所以导致 poll 误差太大?你在 poll 直接 sleep 一段时间看看呢
    yangyuhan12138
        13
    yangyuhan12138  
    OP
       2020-02-13 14:03:23 +08:00
    @ppyybb
    1.线程是同步创建的,循环完了线程应该就创建完了,不存在等待线程创建地说法
    2.take 方法执行到 notEmpty.await()的时候 这些 node 确实会 condition 队列里,但是 take 方法一进来就有 takeLock.lockInterruptibly()抢锁的操作,理想状态 100000 个线程同时抢锁只有一个抢到其余的应该进 AQS 队列(事实上并没有这么多线程进入队列,因为非公平锁和线程并不是同时启动的原因)
    3.此时主线程调用 poll(1000, TimeUnit.MILLISECONDS)方法,这个方法会抢两次锁第一次为 takeLock.lockInterruptibly(),第一次抢锁的时间并没有计入等待时间,然后执行 notEmpty.awaitNanos(nanos),挂起当前线程,等待传入时间结束之后唤醒当前线程,并将当前 node 移入 AQS 队列,再次抢锁,由于两次抢锁都是非公平的所以都有可能一来就抢到,而不进 AQS 队列,理想状态为两次都进入队列,然后等待其他线程挨个 unlock 唤醒下个线程,这样时间就有可能会长
    ppyybb
        14
    ppyybb  
       2020-02-13 15:30:15 +08:00 via iPhone
    @yangyuhan12138

    关于 1,我的意思是线程本身没有执行完,不是说线程没有创建完。

    其它部分又仔细看了下代码,你说的应该是对的。一开始看的不仔细。

    要验证下应该也比较容易,只需要确保每一个 take 都直接执行到 await 那里,这样就不会因为 lock 进入 sync 队列了。

    这个应该可以通过在 take 前面加一个 flag 用来告知主线程这里快执行到 take 了,然后再 sleep 一小会。之后再执行下一个 submit。

    这种情况下进入 sync 队列的应该非常少或者没有,来对比下会不会出现误差大的情况。

    但是多说一句,这么大线程数量的情况下,任何 poll 不可靠都是符合逻辑的,因为 os 调度总需要线程时间...虽然这个 case 里面让别的线程暂时 sleep 住了。实际场景中应该不需要考虑这种情况。
    yangyuhan12138
        15
    yangyuhan12138  
    OP
       2020-02-13 19:51:31 +08:00
    @ppyybb 我试过的,直接主线程睡会儿再执行 poll(1000, TimeUnit.MILLISECONDS)就好了,只要 AQS 队列里没有排队的 node 这个方法还是很及时的,主要就是看是否有大量线程在抢锁,如果有,那么 poll(1000, TimeUnit.MILLISECONDS)就可能超过指定时间返回,awaitNanos(nanos)相比 await 只是多了一个自我唤醒的步骤,唤醒之后还是得抢锁,但是由于非公平锁的原因确实不好测试,有可能两次抢锁都一下就抢到了而不仅 AQS 队列
    ppyybb
        16
    ppyybb  
       2020-02-13 20:15:44 +08:00 via iPhone
    @yangyuhan12138 如果你指主线程再 poll 前只 sleep 一下我认为不能说明问题,这个只影响上面的线程是否执行完毕全部进入阻塞状态,而不影响进入 sync 队列的 node 数量。你这样对测试结果反而看起来更像线程太多影响调度导致而不是 node 太多导致。

    如果指的是每次 submit 前 sleep 一下才有说服力是 node 太多导致的。

    非公平锁也不能测试啊,你拷贝一份 queue 的代码把锁换了,而且可以测试更多自定义的操作。
    yangyuhan12138
        17
    yangyuhan12138  
    OP
       2020-02-13 20:26:30 +08:00
    @ppyybb 我说的就是主线程 poll 之前 sleep 一下,确保上边的线程执行完毕全部进入阻塞状态,如果上边的线程全部都在 await 的话入 sync 队列的 node 数量就是 0 了呀,这样就不会有延迟
    我是说这样不会有问题
    你应该想说的为了证明这个问题 应该在每次 submit 的时候睡一下确保上一个线程在执行 takeLock.lockInterruptibly()?
    ppyybb
        18
    ppyybb  
       2020-02-13 20:29:30 +08:00
    @yangyuhan12138 又想了下,直接 sleep 一次也是可以说明问题,因为上面没有活跃线程,那么在 unfair lock 的情况下,无论是加锁还是超时都应该一次抢到,因此不用进入 sync 队列, 这个逻辑说的通。

    不过公平锁还是可以通过自己拷贝一份 Queue 然后换掉来测试,当然似乎没有这个必要了~
    ppyybb
        19
    ppyybb  
       2020-02-13 20:33:32 +08:00
    @yangyuhan12138 哦,你说的这个也对,await 的时候会释放锁,从而不在 sync 队列里面了。从这个方面来说也是对的。
    yangyuhan12138
        20
    yangyuhan12138  
    OP
       2020-02-13 20:43:57 +08:00
    在每次 submit 的时候睡一下是不科学的一是因为时间原因,每个线程都睡太耗时间了;二是因为这样也不能确保所有线程同时 take,睡了的话上一个线程反而会执行到 await 把锁释放了
    yangyuhan12138
        21
    yangyuhan12138  
    OP
       2020-02-13 20:44:23 +08:00
    @ppyybb 在每次 submit 的时候睡一下是不科学的一是因为时间原因,每个线程都睡太耗时间了;二是因为这样也不能确保所有线程同时 take,睡了的话上一个线程反而会执行到 await 把锁释放了
    ppyybb
        22
    ppyybb  
       2020-02-13 20:50:25 +08:00 via iPhone
    @yangyuhan12138
    其实这样做的目的就是不需要所有线程同时 take 啊,不发生冲突就不会有入 sync 的问题,我的原意只是让进入 sync 尽量少即可说明问题。

    当然我想复杂了,因为 aqs 是两年前看的内容了,今天临时翻了下细节不熟悉,搞得我想成进入 sync 后就不会出来,忘记后面还有一个 await 会释放锁从而最终所有 node 都会出 sync 了。
    yangyuhan12138
        23
    yangyuhan12138  
    OP
       2020-02-13 20:57:51 +08:00   ❤️ 1
    @ppyybb 哦哦,原来是这样,两年前的内容都还记得也是很厉害了,我是因为疫情原因,在家没事干,学习下 AQS,发现这里可能会有点问题就提出来大家一起讨论一下,也感谢大家跟我一起讨论,祝大家身体健康,工作顺利
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   808 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 21:16 · PVG 05:16 · LAX 14:16 · JFK 17:16
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.