对于所有周五深夜工作的人。。。 JAVA多线程的CSP模型 =================================== 下面是Java的sync/wait/notify原语的CSP模型草案。 这并不像我担心的那么糟糕! 这是应该的。Java 原语不难非正式地解释。。。 所以同样应该 任何正式的解释都是真实的。 如能提供反馈,我将不胜感激 这张草稿有什么问题。 我们从结局开始。。。 结论: ============ 假设这个模型成立,它为我们提供了Java的形式化语义 多线程。 尽管遇到了所有问题并发出了警告 关于这个主题,我在Java社区中很少看到对 没有这样的东西。 但如果没有它,我们将始终感到不舒服 关于使用多个控制线程的任何产品的安全性。 这里给出的特定语义是一个*CSP*语义。 The importance of the*CSP*是一个并发系统的代数——一个形式化的 我们可以精确指定需求的数学(包括 比如死锁自由)并证明我们的实现满足 他们。 此外,一些强大且成熟的CSP工具(例如,来自Formal的FDR 可以应用Systems Ltd.和Jeremy Martin的死锁/卫星检查器)。 这意味着任何多线程Java代码——不仅仅是使用 直接访问CSP原语的JCSP或CTJ库成为 易于进行正式和(部分)自动化分析。 如果对此不感兴趣 Java社区,那么我们都会遇到很多麻烦。 Java设计人员、实现人员、测试人员和维护人员都满身伤痕 它的多线程原语,但应用程序强制使用它们。 未来, 如果还没有,那么财务关键型和安全关键型应用程序将 多线程。 多线程的CSP模型将此应用于实体工程 基础。 没有人可以忽视这一点。 任何这样做的人至少都会, 如果追查到一起乱七八糟的事故,将面临诉讼风险 由于某些竞争条件导致的系统故障: “所以,比尔,你在销售系统时并不知道它是不是 无死锁。。。 你甚至从未尝试过这些标准程序 可能已经发现了?!! 尽管有免责声明,你会吗 说这表明对 你的客户? " 例如,现在应该可以构建一个CSP模型 错误的ALT算法: http://www.hensa.ac.uk/parallel/groups/wotug/java/discussion/4.html 连同压力测试(如“周末”帖子所述)一起应用 Jeremy的死锁检查器,并观察它快速弹出一条可暴露的跟踪 僵局。 哇——我真希望几年前我们有这样的机会! 当然,对于“校正的”ALT算法,我们最好重复这一点 看看有没有什么恶心的东西冒出来! 假设没有;-), 我们可以搬家 证明这是ALT的正确实施。 大多数使用sync/wait/notify原语的Java多线程代码-- 包括我自己的代码——看起来很可疑。 正如托尼·霍尔(Tony Hoare)在1980年所说 图灵奖演讲中,销售两种计算机系统: o那些明显正确的。。。 o和那些没有明显错误的。。。 他还指出,当然,生产后者要容易得多。 猜猜我们在兜售哪种! 我想知道会有多少惊喜出现 当我们开始将CSP工具应用于Java代码时? 语法: ======= 这个模型是用霍尔书中的CSP版本构建的——所以它是 恐怕有点过时了。 另外,我不知道CSP的正确ASCII语法,所以我自己编。 下标: “A-subscript-i”将以数组表示法写入“A[i]” 复制的运算符: 执行对某个集合进行索引的外部选择(例如)-例如。 []事件[i]-->P X中的i 将写在一行中: [](X中的事件[i]-->P|i) 字母表: 进程“P”感兴趣的事件集是“alpha(P)” 对象和线程: ==================== 让“Objects”是所有Java对象的编号。 对于CSP建模的任何特定Java应用程序,可以是 仅限于应用程序中任何线程所在的对象 始终“同步”。 通常,这将是有限的和小的。 让“线程”是所有Java线程的编号。 对于CSP建模的任何特定Java应用程序,可以是 仅限于创建和启动的线程。 有时,这可能是无限的或很大的。 同步: ============= 让“声明”和“发布”成为事件集: claim={claim[o][t]|o在Objects中,t在Threads}中 release={release[o][t]|o在Objects中,t在Threads}中 设“LOCK”为一组进程: LOCK={对象中的LOCK[o]|o} 每个“LOCK[o]”进程只是一个二进制信号量——即。 alpha(LOCK[o])=线程}联合中的{claim[o][t]|t {线程中的释放[o][t]|t} LOCK[o]=[](在线程中声明[o][t]->释放[o][t]->LOCK[o]|t) 因此,一旦线程声明“LOCK[o]” 同一个线程将释放它 “LOCK[o]”将被拒绝。 Java“synchronized”块: 同步(o){ P(P) } 建模依据: (权利要求[o][me]-->P); (释放[o][me]-->跳过) 其中“me”是执行此代码的线程的索引。 的主体 块“P”是(递归建模为)一个进程。 请注意,当存在竞争过程时,没有定义哪个过程 拿到锁。 “锁定[o]”过程没有承诺如何竞争 对索赔人进行管理(例如,采用先进先出法)。 这是定义的(或者更确切地说, 未定义)。 这很容易! 实际上,这太容易了:-(。。。 Java允许锁拥有线程“自动”重新声明/释放锁 (例如,当“synchronized”方法在 同一对象)。 在“LOCK[o]”中对此类索赔/释放堆栈进行建模 由于必须处理一个锁线程,这个过程会很复杂 通过“等待”让它离开——因为它必须重新认领锁(如果和何时 在相同的堆栈级别和任意数量的线程上 不同的堆栈级别,可能会在过渡期间将其锁定)。 因此,让我们不要使用“LOCK[o]”,而是在 任何合理的实现都是如此。 每个线程(索引“me”)为任何对象维护一个计数“count[o][me]”, “o”,在其上可以“同步”。 每个“count[o][me]”都已初始化 至零。 然后,免除occam-style语法,Java“synchronized”块: 同步(o){ P(P) } 建模依据: 国际单项体育联合会 计数[o][me]=0 (索赔[o][me]->P'); (释放[o][me]-->跳过) count[o][me]>0 P’ 其中,对“++”/“--”深表歉意(但它只适合一行): P'=计数[o][me]++; P; 计数[o][me]-- 也就是说,线程不需要在任何索赔/发布事件上进行同步 如果它已经有锁了! 等待并通知: ================ 让“wait-a”、“wait-b”和“notify”成为事件集: wait-a={wait-a[o][t]|o在Objects中,t在Threads}中 wait-b={wait-b[o][t]|o在Objects中,t在Threads}中 notify={notify[o][t]|o在Objects中,t在Threads}中 让“等待”成为一组过程: WAIT={对象中的WAIT[o]|o} 哪里: 线程}联合中的alpha(WAIT[o])={WAIT-a[o][t]|t 线程}联合中的{wait-b[o][t]|t {在线程中通知[o][t]|t} WAIT[o]=等待[o][0] 哪里: α(WAIT[o][n])=α(WAIT[o]),对于所有n>=0 WAIT[o][0]=([](线程中的WAIT-a[o][t]-->WAIT[o][1]|t)) [] ([](通知线程中的[o][t]-->WAIT[o][0]|t) 其中,对于n>0: WAIT[o][n]=([](线程中的WAIT-a[o][t]-->WAIT[o][n+1]|t)) [] ([](通知[o][t]--> [](线程中的wait-b[o][s]-->wait[o][n-1]|s) |线程中的t)) 因此,每个对象“o”都有一个“WAIT[o]”过程。 最初,这有 计数字段(由“WAIT”的第二个索引表示)设置为零。 这个 count保存正在上执行Java“wait()”的线程数 对象“o”。 Java代码: o.wait(); 建模依据: 释放[o][me]-->wait-a[o]][me]-->wait-b[o][我]-->claim[o][Cme]-->跳过 其中“me”是执行此代码的线程的索引。 请注意,这些 操作的发生与“count[o]”的值无关,并且不更改它。 Java中的约束之一是“o.wait”(或“o.notify”)只能 由当前持有“o”监视器锁的线程调用,即。 从“synchronized(o)”块内部。 如果线程违反了此约束, 上述与“LOCK[o]”并行运行的“o.wait()”模型表示 调用线程将被永久阻塞(即死锁)。 这似乎是一种合适的惩罚(并且应该可以通过 CSP分析工具)。 此序列中的第一个“release[o][me]”释放“LOCK[o]”信号量。 假设应用程序没有违反上述约束,这将 肯定会发生,因为“LOCK[o]”将等待“release[o][me]” (只有这个线程会发送)。 接下来,这个序列中的“wait-a[o][me]”总是可以被 等待“WAIT[o][n]”进程并增加其内部计数。 请注意,不能保证“WAIT[o][n]”过程*将*永远 接受“等一等我”。 就像在Java中一样,不能保证 一个等待的线程会被注意到,然后被释放。 假设“wait-a[o][me]”发生了,线程现在阻塞在 “wait-b[o][me]”事件。 直到“WAIT[o][n]” 进程接收到“notify[o][t]”。 请参见下文。 最后,这条线必须与所有竞争对手竞争,“宣称自己” 回收“LOCK[o]”。 Java代码: o.notify(); 建模依据: 通知[o][me]-->跳过 其中“me”是执行此代码的线程的索引。 请注意 不涉及释放并随后重新声明“LOCK[o]”, 这是根据Java(到目前为止尚未规范化)的规则。 目前, 该模型确实允许调用“o.notify()”,而无需保留 “LOCK[o]”——但这将在下一节中处理。 对于“WAIT[o][n]”流程,此“通知我”始终是可以接受的。 如果“n”为零,则没有线程等待“o”和通知 被接受但被忽略。 这是Java所要求的。 如果“n”大于零,则至少有一个线程在“o”上等待。 任何 等待“o”的线程“s”将尝试在“wait-b[o][s]”上同步。 因此,当“WAIT[o][n]”接受“notify[o]][t]”时,它将接受其中一个 “wait-b[o][s]”事件。 如果提供多个,则为任意选择 制造完成。 这符合Java的(非正式)规则——没有承诺 在收到通知后,公平地释放等待线程。 接受后 其中一个“wait-b[o][s]”事件是“wait[o]][n]”,它会减少计数。 幸运线程“s”刚刚从其“wait-b[o][s]”中释放出来, 现在必须重新获取“o”上的锁——请参阅中最后的“claim[o][me]” “o.wait()”模型。 没有获得此项的特殊特权 锁定。。。 它必须与所有其他试图声称它的线程竞争 ……因此出现了“什么,没有鸡?”的困难! 这种可悲的行为是 此CSP正确捕获。 一个重要的观察结果是,“WAIT[o][n]”永远不会停留在等待中 对于“wait-b[o][s]”——至少有一个始终可用。 因此, 我们可以保证,对于“n”的*某些*值,“WAIT[o][n]”将始终 能够服务任何“wait-a[o][t]”或“notify[o][t]”事件。 这是 什么是上面两次使用的短语“总是可以接受的”(和 以下)。 上述模型并没有强制要求“notify[o][me]”总是 接受——只是它总是可以接受的。 一个Java线程正在执行 “o.notify()”可能不应该无限期地阻塞,但应该始终阻塞 释放等待线程后终止(如果存在)。 然而, 必须进行一些同步,因为数据结构是共享的 必须检查并更新“o.wait()”方法。 可能是这样 同步是“公平的”,并保证最终为 “o.notify()”。。。 但我还没有看到(非正式) Java规范? 鉴于未就进入做出任何承诺 从“等待”到“同步”块或最终释放,这样的承诺 将不一致。 如果出现特殊版本的Java(例如用于实时应用程序) *承诺最终进入“synchronized”块并发布 从“o.wait()”方法开始,我们必须用扩展的CSP对它们建模 它抓住了“最终”或“公平”为活动提供服务的概念。 阿德里安·劳伦斯的CSPP解决了优先权的概念 能够做到这一点。 监控同步和等待/通知规则: ====================================== 上述CSP模型假设的Java规则是“o.wait()”和 “o.notify()”只能由当前持有 锁定对象“o”。 该规则由Java运行时系统执行。 违反此规则的Java应用程序可能会编译,但会出现异常 在运行时抛出。 要签出不违反此规则的Java应用程序 上述模型就足够了。 然而,如果我们担心 在CSP中建模很简单。 如前所述,“o.wait()”规则已经由 当前模型。 要获得“o.notify()”的规则,最简单的方法是 是修改“LOCK[o]”过程,以包括所有“notify[o][t]” 事件。 然后,它可以拒绝来自具有 没有声称。。。 并接受来自具有以下功能的线程的“通知”: alpha(LOCK[o])=线程}联合中的{claim[o][t]|t 线程}联合中的{release[o][t]|t {在线程中通知[o][t]|t} LOCK[o]=[](在线程中声明[o][t]-->LOCKED[o][t]|t) 哪里: 阿尔法(锁定[o][t])=阿尔法(锁定[o]) LOCKED[o][t]=(释放[o][t]-->LOCK[o])[](通知[o][C]-->LOCKED[o][t]) 通知所有人: =========== 要完成Java线程同步模型,我们需要捕获 “o.notifyAll()”。 这意味着释放所有线程 正在等待“o”,即已执行“o.wait()”。 如果没有线程 正在等待,这很好,调用不会导致任何结果。 这很容易:定义一组“notifyAll[o][t]”方法,添加相关的 将“LOCK[o]”和“WAIT[o] 变化。 “LOCK[o]”扩展防止线程执行时出现“notifyAll[o][t]” “t”没有要求。 “WAIT[o]”扩展目前发布了所有 等待线程——没有新线程能够执行“o.wait()” 直到完成。 完整的模型将在下一节完整地介绍。 模型概要: ===================== 集合(可Ennumerable): ------------------- 对象,线程 活动: ------- claim={claim[o][t]|o在Objects中,t在Threads}中 release={release[o][t]|o在Objects中,t在Threads}中 wait-a={wait-a[o][t]|o在Objects中,t在Threads}中 wait-b={wait-b[o][t]|o在Objects中,t在Threads}中 notify={notify[o][t]|o在Objects中,t在Threads}中 notifyAll={notifyAll[o][t]|o在Objects中,t在Threads}中 `JVM的进程: ---------------- 每个Object有两个过程: LOCK={对象中的LOCK[o]|o} WAIT={对象中的WAIT[o]|o} 哪里: alpha(LOCK[o])={线程中的声明[o][t]|t}并集 线程}联合中的{release[o][t]|t 线程}联合中的{notify[o][t]|t {线程中的notifyAll[o][t]|t} LOCK[o]=[](在线程中声明[o][t]-->LOCKED[o][t]|t) 其中: α(锁定[o][t])=α(锁定[o]) 锁定[o][t]=(释放[o][t]-->锁定[o])][] (通知[o][t]-->锁定[o][t])[] (通知所有[o][t]-->锁定[o][t]) 也: alpha(WAIT[o])={线程中的WAIT-a[o][t]|t}并集 线程}联合中的{wait-b[o][t]|t 线程}联合中的{notify[o][t]|t {线程中的notifyAll[o][t]|t} WAIT[o]=等待[o][0] 哪里: α(WAIT[o][n])=α(WAIT[o]),对于所有n>=0 WAIT[o][0]=([](线程中的WAIT-a[o][t]-->WAIT[o][1]|t)) [] ([](通知线程中的[o][t]-->WAIT[o][0]|t) [] ([](notifyAll[o][t]-->WAIT[o][0]|t在线程中) 其中,对于n>0: WAIT[o][n]=([](线程中的WAIT-a[o][t]-->WAIT[o][n+1]|t)) [] ([](通知[o][t]--> [](线程中的wait-b[o][s]-->wait[o][n-1]|s) |螺纹中的t)) [] ([](notifyAll[o][t]-->RELEASE[o][n]|t在线程中) 其中: α(释放[o][n])=α(等待[o]),对于所有n>=0 释放[0][0]=等待[o][0] 其中,对于n>0: RELEASE[o][n]=[](线程中的等待-b[o][2s]-->RELEASE[o][1]|s) 计数(可以是进程): ---------------------------- count={count[o][t]|o在Objects中,t在Threads}中 用户进程: --------------- 应用程序/小程序/whateveret中的每个启动线程都是一个进程。 这些过程形成了ennumerated集合: USER={Users}中的USER[me]|me 其中Users是(所有Java)Threads的子集。 USER[me]的字母表为: alpha(USER[me])=Objects}联合中的{claim[o][me]|o 对象}联合中的{release[o][me]|o 对象}联合中的{wait-a[o][me]|o 对象}联合中的{wait-b[o][me]|o {通知对象中的[o][me]|o}联合 {对象中的notifyAll[o][me]|o} 对于每个“o”,每个“USER[me]”必须将“count[o][me”初始化为零 在对象中。 Java“synchronized”块: 同步(o){ P(P) } 是“用户[我]”生活的一部分,对于某些“我”来说,由以下内容建模: 国际单项体育联合会 计数[o][me]=0 (索赔[o][me]-->P'); (释放[o][me]-->跳过) count[o][me]>0 P’ 哪里: P'=计数[o][me]++; P; 计数[o][me]-- Java代码: o.wait(); 建模依据: 释放[o][me]-->wait-a[o]][me]-->wait-b[o][我]-->claim[o][Cme]-->跳过 Java代码: o.notify(); 建模依据: 通知[o][me]-->跳过 Java代码: o.notifyAll(); 建模依据: notifyAll[o][me]-->跳过 一个完整的系统: ------------------ 一个完整的Java多线程系统就是 所有“JVM”和用户进程: (||(在对象中等待[o]|o)) || (||(对象中的LOCK[o]|o) || (||(用户中的USER[t]|t) 结论: ============ 正面看! 彼得·韦尔奇。 (1999年3月26日)