了解 AQS 的进来讨论一下

2020-02-11 21:09:34 +08:00
 yangyuhan12138

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

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

2786 次点击
所在节点    Java
23 条回复
ppyybb
2020-02-11 21:55:37 +08:00
会超过,但是 nanosecond 的精度很高了,差异很小,这就是为什么这个 api 要用纳秒做参数的原因,实际中应该可以忽略误差。
optional
2020-02-11 21:58:12 +08:00
这个误差是可接受的,换个角度说,因为 STW 的存在,对时间很敏感的根本就不应该用带 GC 的语言。
yangyuhan12138
2020-02-11 22:31:52 +08:00
@ppyybb 但是极端情况下:比如有多个线程在 poll,他们都加入了 AQS 等待队列,这个时候 awaitNanos 超时时间到了再把 awaitNanos 这个线程从 Condition 里的链表转移到 AQS 里的链表,由于先进先出原则.调用 awaitNanos 的这个线程得等到 AQS 之前的所有 Node 都出队之后才会到他去抢锁,所以这个值也可能很大甚至误差几秒钟都可能
yangyuhan12138
2020-02-11 22:33:14 +08:00
@optional 可能也有点关系吧
ppyybb
2020-02-11 23:34:57 +08:00
@yangyuhan12138 你这个几秒完全没有根据啊,你前面得有多少 node 才会有这么大误差?我觉得完全没有必要考虑这种不切实际的情况
mreasonyang
2020-02-12 03:27:14 +08:00
会超,但除非系统挂了,不然不可能超几秒,如果能模拟这种情况可以贴下代码讨论下
lu5je0
2020-02-12 13:00:36 +08:00
@yangyuhan12138 而且 LinkedBlockingDeque 中使用的是非公平的 ReentrantLock 锁,不会等到所有 node 都出队之后才去抢锁。
yangyuhan12138
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
2020-02-13 02:21:23 +08:00
@mreasonyang
@ppyybb
@lu5je0
@optional
大致代码如上 理论上线程数量越多 AQS 里的链表越长,时间越长,但是正如 @lu5je0 所说的,AQS 默认实现为非公平锁,有可能一来就直接拿到锁而不进 AQS 链表,所以结果为 1s,但是如果他进了 AQS 链表就会产生误差,我电脑只能跑到 10w 线程 如图所示
yangyuhan12138
2020-02-13 02:25:47 +08:00
yangyuhan12138
2020-02-13 02:29:40 +08:00
淦 为啥我的图不显示
ppyybb
2020-02-13 12:27:10 +08:00
@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
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
2020-02-13 15:30:15 +08:00
@yangyuhan12138

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

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

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

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

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

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

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

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

不过公平锁还是可以通过自己拷贝一份 Queue 然后换掉来测试,当然似乎没有这个必要了~
ppyybb
2020-02-13 20:33:32 +08:00
@yangyuhan12138 哦,你说的这个也对,await 的时候会释放锁,从而不在 sync 队列里面了。从这个方面来说也是对的。
yangyuhan12138
2020-02-13 20:43:57 +08:00
在每次 submit 的时候睡一下是不科学的一是因为时间原因,每个线程都睡太耗时间了;二是因为这样也不能确保所有线程同时 take,睡了的话上一个线程反而会执行到 await 把锁释放了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/643848

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX