前几天读LinkedTransferQueue(以下简称ltq)的源码,想加深下对松弛型双重队列的理解,无意中发现了这个问题:),经过仔细检查后确认了这是个bug,存在于JDK1.7.0_40和刚发布的JDK8中,去google和oracle官方似乎也没有搜索到这个问题。
重现bug:先来重现下这个bug,由于对并发线程的执行顺序预先不能做任何假设,所以很可能根本就不存在所谓的重现错误的“测试用例”,或者说这个测试用例应该是某种“执行顺序"。所以我一开始的做法是copy了一份ltq的源码,通过某个地方加自旋...但是这种方法毕竟要修改源码,后来我发现直接debug进源码就可以轻易重现bug了。
LinkedTransferQueue:xfer(E e, boolean haveData, int how, long nanos) if (how != NOW) { // No matches available if (s == null) s = new Node(e, haveData); Node pred = tryAppend(s, haveData); if (pred == null) continue retry; // lost race vs opposite mode if (how != ASYNC) return awaitMatch(s, pred, e, (how == TIMED), nanos); } return e; // not waiting
在以上06行Node pred = tryAppend(s, havaData) 断点(我是windows下用eclipse调试);
debug以下代码:
public static void main(String[] args) { final BlockingQueue<Long> queue = new LinkedTransferQueue<Long>(); Runnable offerTask = new Runnable(){ public void run(){ queue.offer(8L); System.out.println("offerTask thread has gone!"); } }; Runnable takeTask = new Runnable(){ public void run(){ try { System.out.println(Thread.currentThread().getId() + " " +queue.take()); } catch (InterruptedException e) { e.printStackTrace(); } } }; Runnable takeTaskInterrupted = new Runnable(){ public void run(){ Thread.currentThread().interrupt(); try { System.out.println(Thread.currentThread().getId() + " " +queue.take()); } catch (InterruptedException e) { System.out.println(e + " "+Thread.currentThread().getId()); } } }; new Thread(offerTask).start(); new Thread(takeTask).start(); new Thread(takeTaskInterrupted).start(); }
执行到断点处之后,在Debug界面里面有Thread-0、Thread-1、Thread-2三个线程分别指代代码中的offerTask、takeTask、takeTaskInterrupted三者。现在执行三步:
step 1: Resume Thread-1(没有输出,线程Thread-1自己挂起,等待数据)
step 2: Resume Thread-2(看到类似于 java.lang.InterruptedException 15 的输出)
step 3: Resume Thread-0(输出:offerTask thread has gone!)
offer线程已经执行完毕,然后我们的64L呢,明明Thread-1在等待数据,数据丢失了吗?其实不是,只不过take线程现在无法取得offer线程提交的数据了。
如果你觉得上面的数据丢失还不是什么大问题请在上面的示例下添加如下代码(和你CPU核心数相同的代码行:)
.............. new Thread(takeTask).start(); new Thread(takeTask).start(); new Thread(takeTask).start(); new Thread(takeTask).start();
把上面的3个step重新按顺序执行一遍,建议先打开任务管理器,接着忽略断点,让接下来这几个线程跑:)
CPU爆满了吧...其实是被这几个线程占据了,你去掉几行代码,CPU使用率会有相应的调整。
所以这个bug可能会引起数据暂时遗失和CPU爆满, 只不过貌似发生这种情况的概率极低。
原因:为什么会出现这个bug呢,要想了解原因必须先深入分析ltq内部所使用的数据结构和并发策略,ltq内部采用的是一种非常不同的队列,即松弛型双重队列(Dual Queues with Slack)。
数据结构:
松弛的意思是说,它的head和tail节点相较于其他并发列队要求上更放松,构造它的目的是减少CAS操作的次数(相应的会增加next域的引用次数),举个例子:某个瞬间tail指向的节点后面已经有6个节点了(以下图借用源码的注释-_-|||没画过图),而其他并发队列真正的尾节点最多只能是tail的下一个节点。
* head tail
* | |
* v v
* M -> M -> U -> U -> U -> U->U->U->U->U
收缩的方式是大部分情况下不会用tail的next来设置tail节点,而是第一次收缩N个next(N>=2),然后查看能否2个一次来收缩tail。(head类似,并且head改变一次会导致前“head"节点的next域断裂即如下图)
*"prehead" head tail
* | | |
* v v v
* M M-> U -> U -> U -> U->U->U->U->U
双重是指有两种类型相互对立的节点(Node.isData==false || true),并且我理解的每种节点都有三种状态:
1 INIT(节点构造完成,刚进入队列的状态)
2 MATCHED(节点备置为“满足”状态,即入队节点标识的线程成功取得或者传递了数据)
3 CANCELED(节点被置为取消状态,即入队节点标识的线程因为超时或者中断决定放弃等待)
(bug的原因就是现有代码中将2、3都当做MATCHED处理,后面会看到把3独立出来就修复了这个问题)
并发策略:
既然使用了松弛的双重队列,那么当take、offer等方法被调用时执行的策略也稍微不同。
就我们示例中的代码的流程来看,Thread-0、Thread-1、Thread-2几乎同时进入到了xfer的调用,发现队列为空,所以都构造了自己的node希望入队,于是三者都从tail开始加入自己的node,我们在这里的顺序是Thread-1->Thread-2->Thread-0,因为想要入队还要和当前的tail节点进行匹配得到“认可”才能尝试入队,队列为空Thread-1理所当然入队成功并且挂起了自己的线程(park)等待相对的调用来唤醒自己(unpark),然后Thread-2发现队列末尾的node和自己是同一类型的,于是通过了测试把自己也加入了队列,由于本身是中断的所以让自己进入MATCHED状态(bug就是这里了,上面说过CANCEL被当做MATCHED状态处理),接着我们提交数据的Thread-0来了,发现末尾节点的类型虽然对立但却是MATCHED状态(假如不为MATCHED会有退回再从head来探测一次的机会),所以认为队列已经为空,前面的调用已经被匹配完了,然后把自己的node入队,这样就形成了如下所示的场景:
* Thread-1 Thread-2 Thread-0
* | | |
* v v v
* REQUEST -> MATCHED -> DATA
好了, 现在Thread-3来了,先探测尾部发现Thread-0的node是类型相反的,于是退回从头部开始重新探测,但是又发现Thread-1的node的类型是相同的,于是再次去探测尾部看看能否入队.......结果造成CPU是停不下来的。
修复:
如上面所说,错误的本质在于当尾部的节点是CANCELED(取消)状态时不能作为被匹配完成的MATCHED状态处理,应该让后来者回退到head去重新测试一次所以重点是对源码做出如下修改(修改放在注释中):
static final class Node { final boolean isData; // false if this is a request node volatile Object item; // initially non-null if isData; CASed to match volatile Node next; volatile Thread waiter; // null until waiting static final Object CANCEL = new Object(); final void forgetWaiter(){ UNSAFE.putObject(this, waiterOffset, null); } final boolean isCanceled(){ return item == CANCEL; }
在Node节点代码中加入标识取消的对象CANCEL。
private E xfer(E e, boolean haveData, int how, long nanos) { if (item != p && (item != null) == isData /*&& item!=Node.CANCEL*/) { // unmatched if (isData == haveData) // can't match
在xfer函数中添加对于为状态为取消的判断。
private E xfer(E e, boolean haveData, int how, long nanos) { Node pred = tryAppend(/*s,*/ haveData); ..... } private Node tryAppend(Node s, boolean haveData) { else if (p.cannotPrecede(/*s, */haveData)) else { /* if(p.isCanceled()) p.forgetContents();*/ if (p != t) { // update if slack now >= 2
添加对于前置节点为取消状态时当前节点的入队策略
final boolean cannotPrecede(boolean haveData) { boolean d = isData; Object x; return d != haveData && (x = item) != this && (x != null) == d; } final boolean cannotPrecede(Node node, boolean haveData) { boolean d = isData; if(d != haveData){ Object x = item; if(x != this && (x!=null) == d && x!= Node.CANCEL) return true; if(item == CANCEL){ if(node.next != this){ node.next = this; return true; } this.forgetContents(); } } node.next = null; return false; }
这一步是关键, 当我们入队时发现前置节点是类型对立并且取消状态时,我们就需要多一次的回退探测,所以借用了一下next域来标识这个CANCEL节点,下次经过时或者可以确认它可以当做MATCHED处理(它前面没有INIT节点)或者已经有别的节点粘接在它后面,我们就进而处理那个节点,总之当我们总是能够得到正确的行为。
private E awaitMatch(Node s, Node pred, E e, boolean timed, long nanos) { if ((w.isInterrupted() || (timed && nanos <= 0)) && s.casItem(e, /*Node.CANCEL*/)) { // cancel unsplice(pred, s); return e; }
这一处关键点把item的值从原来的s本身修改为我们新增的CANCEL。
额 代码好乱,关于这个bug定位应该没问题,后面的原因很多方面都没讲,剩下的还有很多处大大小小的修改=_=,整个修改之后的LinkedTransferQueue在github上,大家有兴趣的话可以参考下,已经通过了 JSR166测试套件。
相关推荐
微软刚出了WIN10LTSC2021,但是输入法有个重大BUG,无选字框,并且此BUG会造成CPU占用极高,CPU温度过高,CPU散热风扇转速过快,噪音很大等。此工具可以修复此问题,且支持离线系统修复。注意,离线修复只能修复未...
* 安全性 BUG:包括数据有效性检测不合理、重要数据在传输中没有加密、缺少身份认证机制或认证不合理、数据产生缺乏随机性、网络安全性等。 * 可靠性 BUG:包括数据存贮的可靠性、业务处理的可靠性、硬件可靠性、...
bug: fix a ton of old, broken cpu opcodes and CP15 logic bug: return Z1 and Z2 from TSC (fixes some touch logic) bug: gba slot save type detection improved bug: handle unusual rom headers more ...
微软最新的WIN10 LTSC 2021终于出来了,基于WIN10 21H2版本...于是安装了原本体验了一下,发现了史上以来最大的BUG。在这里描述一下问题现象,和网上找来的解决方案总结,避免大家走弯路。解决WSAPPX进程占用CPU超高。
4. 安全性Bug:这类Bug可能导致数据泄露、权限滥用或其他安全风险。测试人员需要进行安全性审计以发现潜在威胁。 5. 用户界面(UI)和用户体验(UX)Bug:界面设计不合理、文字排版错误、交互不流畅等,都可能影响用户...
修复BUG: 1、cxGridWizard文件损坏造成编译报错; 2、cxPivotGridAdvancedCustomization的BUG; FindPanel - The % and _ wildcards work when the UseExtendedSyntax property is disabled 此BUG说明地址:...
本主题将深入探讨bug数据分析在软件工程中的应用以及与Junit测试框架的关联。 首先,我们要理解bug数据分析的重要性。通过对bug的数据分析,我们可以发现软件的薄弱点,了解错误发生的模式,找出最常出现的问题类型...
修复bug:填空题空格数不能多于10个 修复bug:财付通支付bug 修复bug:考试次数限制1个误差 修复bug:扣分和漏选得分不能是小数的bug 修复bug:批量上传错误提示问题 修复漏洞:修复了一个注入漏洞 ...
2. 功能性Bug:如操作无效、功能实现错误、数据处理不准确等。 3. 用户体验Bug:包括界面布局、文字排版、颜色搭配等视觉方面的问题。 4. 系统行为Bug:如数据存储异常、权限控制不当、网络错误等。 5. 兼容性Bug...
* 解决BUG: [XWAN] 修复12.04.0011版本非4WAN时出现CPU占用率100%的BUG ===== 2012-4-1 | 12.04.0011 内测版发布 ===== * 解决BUG: [MAC克隆] 修复12.04.0009版本MAC克隆页面点击保存按钮后浏览器提示JavaScript...
在这个过程中,bug统计分析图表成为了一种有效的数据可视化手段,帮助测试团队和项目管理者快速理解bug的分布情况、严重程度以及解决进度,从而做出更精准的决策。 ### 二、bug统计分析图表的作用 1. **可视化bug...
修复bug:填空题空格数不能多于10个修复bug:财付通支付bug修复bug:考试次数限制1个误差修复bug:扣分和漏选得分不能是小数的bug修复bug:批量上传错误提示问题修复漏洞:修复了一个注入漏洞 主要特点:PPFrame...
为了修复这个问题,开发者通常需要深入理解8051的内存结构、I/O映射以及KEIL C51的编译机制。有时,通过更新编译器版本可以解决这类问题。在本例中,提供的压缩包文件名"Keil_C51_9.02a"可能是一个更新后的编译器...
- 开发已修复的 bug:将 bug 状态置为已解决,同时添加说明验证版本号、错误原因、解决办法。 - 开发认为不是 bug:将 bug 状态置为已拒绝,指派给 bug 提出者,同时注明拒绝理由。 - 开发已修复,测试验证通过的...
在IT行业中,bug数据分析是软件开发过程中的关键环节,它涉及到bug管理系统的构建、bug的识别、分析以及如何根据bug的数量和质量来评估测试人员的绩效。以下是对这些知识点的详细阐述: 首先,**bug管理系统建立**...
本文将深入探讨“千年”游戏服务端存在的自身bug,以及如何通过有效的分析和防范措施来彻底杜绝数据库数据异常,如回档和复制问题。在“千年”这款游戏中,玩家的体验很大程度上取决于服务端的稳定运行,因此,理解...
Java中的TimSort是一种稳定、高效的排序算法,被广泛应用于Java集合框架中的`Arrays.sort()`和`Collections....通过深入研究和实践,我们不仅可以修复已知的bug,还能增强对高效排序算法的理解,提升我们的编程技能。
SonarQube是一款强大的静态代码分析工具,用于检测软件中的bug、漏洞和异味(Code Smell)。通过分析项目源代码,SonarQube能够帮助开发者找出潜在的问题,并提供修复建议,以提高代码质量和可维护性。以下是...