8

当我从大约 20 年的基本、Pascal、COBOL 和 C 过程编程的背景中学习 Java 时,我当时认为最难的事情是围绕 OOP 术语和概念来思考。现在有了大约 8 年的扎实 Java 经验,我得出的结论是,使用 Java 和类似的语言(如 C#)进行编程最困难的事情就是多线程/并发方面。

编写可靠且可扩展的多线程应用程序非常困难!随着处理器发展“更宽”而不是更快的趋势,它正迅速变得非常关键。

当然,最难的领域是控制线程之间的交互以及由此产生的错误:死锁、竞争条件、陈旧数据和延迟。

所以我的问题是:你采用什么方法或方法来生成安全的并发代码,同时减少死锁、延迟和其他问题的可能性?我提出了一种有点非传统的方法,但在几个大型应用程序中效果很好,我将分享这个问题的详细答案。

4

15 回答 15

7

这不仅适用于 Java,也适用于一般的线程编程。我发现自己只需遵循以下准则就可以避免大多数并发和延迟问题:

1/ 让每个线程运行自己的生命周期(即决定何时死亡)。它可以从外部提示(比如一个标志变量),但它完全负责。

2/让所有线程以相同的顺序分配和释放它们的资源——这保证不会发生死锁。

3/ 尽可能短的时间锁定资源。

4/ 将数据的责任交给数据本身——一旦你通知线程数据是它要处理的,不要管它,直到责任归还给你。

于 2008-11-02T05:10:20.037 回答
6

有许多技术刚刚进入公众意识(例如:过去几年)。一个大的将是演员。这是 Erlang 首次引入网格铁的东西,但它已被 Scala 等新语言(JVM 上的参与者)发扬光大。虽然演员确实不能解决所有问题,但它们确实使推理代码和识别问题点变得更加容易它们还使设计并行算法变得更加简单,因为它们迫使您使用延续传递共享可变状态的方式。

Fork/Join 是你应该看的东西,特别是如果你在 JVM 上。Doug Lea 撰写了关于该主题的开创性论文,但多年来许多研究人员一直在讨论它。据我了解,Doug Lea 的参考框架计划包含在 Java 7 中。

在侵入性稍小的层面上,通常简化多线程应用程序所需的唯一步骤就是降低锁定的复杂性。细粒度锁定(Java 5 风格)非常适合吞吐量,但很难做到正确。通过 Clojure 获得一些关注的另一种锁定方法是软件事务内存 (STM)。这本质上与传统锁定相反,因为它是乐观的而不是悲观的。您首先假设您不会有任何冲突,然后允许框架在问题发生时解决问题。数据库通常以这种方式工作。这对于碰撞率低的系统的吞吐量非常有用,但最大的胜利在于算法的逻辑组件化。无需将锁(或一系列锁)与某些数据任意关联,您只需将危险代码包装在事务中,让框架解决其余的问题。您甚至可以从像 GHC 的 STM monad 或我的实验性 Scala STM 这样的体面的 STM 实现中获得相当多的编译时检查。

有很多用于构建并发应用程序的新选项,您选择哪一个很大程度上取决于您的专业知识、您的语言以及您尝试建模的问题类型。作为一般规则,我认为演员与持久的、不可变的数据结构相结合是一个可靠的选择,但正如我所说,STM 的侵入性要小一些,有时可以产生更直接的改进。

于 2008-11-02T06:23:50.107 回答
5
  1. 尽可能避免在线程之间共享数据(复制所有内容)。
  2. 在可能的情况下,永远不要锁定对外部对象的方法调用。
  3. 尽可能缩短锁定时间。
于 2008-11-02T04:57:35.037 回答
5

Java中的线程安全没有一个真正的答案。但是,至少有一本非常棒的书:Java Concurrency in Practice。我经常参考它(尤其是我旅行时的在线 Safari 版本)。

我强烈建议您深入阅读本书。您可能会发现,您的非常规方法的成本和收益已经过深入研究。

于 2008-11-02T04:58:37.390 回答
4

我通常遵循 Erlang 风格的方法。我使用主动对象模式。它的工作原理如下。

将您的应用程序划分为非常粗粒度的单元。在我当前的一个应用程序(400.000 LOC)中,我有 appr。这些粗粒度单元中的 8 个。这些单位根本不共享数据。每个单元都有自己的本地数据。每个单元都在自己的线程上运行(= 活动对象模式),因此是单线程的。您不需要单位内的任何锁。当单元需要向其他单元发送消息时,他们通过将消息发布到其他单元的队列来完成。另一个单元从队列中挑选消息并对该消息作出反应。这可能会向其他单位触发其他消息。因此,此类应用程序中唯一的锁围绕队列(每个单元一个队列和锁)。根据定义,这种架构是无死锁的!

一旦您了解了基本原理,该架构的扩展性非常好,并且非常容易实现和扩展。它喜欢将其视为应用程序中的 SOA。

通过将您的应用程序划分为单元记住。每个 CPU 内核的最佳长时间运行线程数是 1。

于 2008-11-02T08:59:23.740 回答
3

我推荐基于流的编程,也就是数据流编程。它使用OOP 和线程,我觉得这像是一个自然的进步,就像 OOP 被程序化了一样。不得不说,数据流编程不是万能的,它不是通用的。

维基百科有关于这个主题的好文章:

http://en.wikipedia.org/wiki/Dataflow_programming

http://en.wikipedia.org/wiki/Flow-based_programming

此外,它还有几个优点,如令人难以置信的灵活配置、分层;程序员(组件程序员)不必编写业务逻辑,它是在另一个阶段完成的(将处理网络放在一起)。

你知道吗,make是一个数据流系统?请参阅make -j,特别是如果您有多核处理器。

于 2010-06-25T20:25:01.430 回答
0

在多线程应用程序中编写所有代码非常……小心!我不知道有什么比这更好的答案。(这涉及到提到的jonnii 之类的东西)。

我听到人们争论(并同意他们)传统的线程模型在未来真的行不通,所以我们将不得不开发一套不同的范例/语言来真正使用这些新奇的多线程模型。有效的核心。像 Haskell 这样的语言,其程序很容易并行化,因为任何具有副作用的函数都必须以这种方式显式标记,还有 Erlang,不幸的是我不太了解。

于 2008-11-02T05:11:24.683 回答
0

我看到的核心问题是(a)避免死锁和(b)在线程之间交换数据。出租人的一个担忧(但只是轻微出租人)是避免瓶颈。我已经遇到了几个不同的无序锁定导致死锁的问题——说“总是以相同的顺序获取锁”很好,但在中型到大型系统中,实际上通常不可能确保这一点。

警告:当我想出这个解决方案时,我必须以 Java 1.1 为目标(所以并发包在 Doug Lea 眼中还不是一闪而过)——手头的工具完全同步并等待/通知。我借鉴了使用基于实时消息的系统 QNX 编写复杂的多进程通信系统的经验。

根据我使用 QNX 的经验,它存在死锁问题,但通过将消息从一个进程的内存空间处理到另一个进程来避免数据并发,我提出了一种基于消息的对象方法——我称之为 IOC,用于对象间协调. 在开始时,我设想我可能会像这样创建所有对象,但事后看来,它们仅在大型应用程序的主要控制点是必需的——如果你愿意的话,“州际交换”并不适用于每一个道路系统中的“十字路口”。事实证明这是一个主要的好处,因为它们完全不是 POJO。

我设想了一个系统,其中对象在概念上不会调用同步方法,而是“发送消息”。消息可以是发送/回复,发送方在处理消息时等待并返回回复,也可以是异步的,其中消息被放入队列并在稍后阶段出列并处理。请注意,这是一个概念上的区别——消息传递是使用同步方法调用实现的。

消息系统的核心对象是IsolatedObject、IocBinding 和IocTarget。

隔离对象之所以被称为是因为它没有公共方法;这是为了接收和处理消息而扩展的。使用反射进一步强制子对象没有公共方法,也没有任何包或受保护的方法,除了那些从 IsolatedObject 继承的方法,几乎​​所有这些方法都是最终的;起初它看起来很奇怪,因为当您继承 IsolatedObject 时,您创建了一个具有 1 个受保护方法的对象:

Object processIocMessage(Object msgsdr, int msgidn, Object msgdta)

其余所有方法都是处理特定消息的私有方法。

IocTarget 是一种抽象IsolatedObject 可见性的方法,对于为另一个对象提供自我引用以将信号发回给您,而不暴露您的实际对象引用非常有用。

IocBinding 只是将发送者对象绑定到消息接收者,这样就不会对发送的每条消息进行验证检查,而是使用 IocTarget 创建的。

与隔离对象的所有交互都是通过“发送”消息来实现的——接收者的 processIocMessage 方法是同步的,这确保了一次只处理一条消息。

Object iocMessage(int mid, Object dta)
void   iocSignal (int mid, Object dta)

在创建了一个孤立对象完成的所有工作都通过一个方法汇集的情况之后,我接下来通过它们在构造时声明的“分类”将对象排列在声明的层次结构中 - 只是一个将它们标识为其中之一的字符串任意数量的“消息接收器类型”,将对象置于某个预定的层次结构中。然后我使用消息传递代码来确保如果发送者本身是一个独立对象,对于同步发送/回复消息,它是层次结构中较低的一个。异步消息(信号)使用线程池中的单独线程分派给消息接收者,线程池的整个工作传递信号,因此信号可以从任何对象发送到系统中的任何接收者。信号可以传递任何所需的消息数据,

因为消息只能向上传递(并且信号总是向上传递,因为它们是由专门为此目的运行的单独线程传递的)死锁在设计上被消除了。

因为线程之间的交互是通过使用 Java 同步交换消息来完成的,所以设计上同样消除了竞争条件和陈旧数据的问题。

因为任何给定的接收器一次只处理一条消息,并且因为它没有其他入口点,所以消除了对对象状态的所有考虑——实际上,对象是完全同步的,并且同步不会意外地被任何方法遗漏;没有 getter 返回过时的缓存线程数据,也没有 setter 在另一个方法作用于它时更改对象状态。

因为只有主要组件之间的交互通过这种机制汇集,所以在实践中这已经很好地扩展了——这些交互在实践中几乎没有我理论的那样经常发生。

整个设计成为以严格控制的方式交互的有序子系统集合之一。

请注意,这不适用于更简单的情况,即使用更传统的线程池的工作线程就足够了(尽管我经常通过发送 IOC 消息将工作线程的结果注入主系统)。它也不用于线程关闭并执行完全独立于系统其余部分的事情的情况,例如 HTTP 服务器线程。最后,它不用于资源协调器本身不与其他对象交互以及内部同步将完成工作而没有死锁风险的情况。

编辑:我应该说交换的消息通常应该是不可变的对象;如果使用可变对象,则发送它的行为应被视为移交并导致发送者放弃所有控制权,并且最好不保留对数据的引用。就个人而言,我使用了一个可锁定的数据结构,它被 IOC 代码锁定,因此在发送时变得不可变(锁定标志是易失的)。

于 2008-11-02T06:11:46.107 回答
0

我建议演员模型。

于 2008-11-02T07:22:02.907 回答
0

演员模型就是您正在使用的,它是迄今为止最简单(和有效的方式)多线程的东西。基本上每个线程都有一个(同步的)队列(它可以依赖于操作系统或不依赖于操作系统),其他线程生成消息并将它们放入将处理消息的线程的队列中。

基本示例:

thread1_proc() {

msg = get_queue1_msg(); // block until message is put to queue1
threat1_msg(msg);

}

thread2_proc() {
msg = create_msg_for_thread1();
send_to_queue1(msg);
}

这是生产者消费者问题的典型例子。

于 2008-11-02T08:53:48.913 回答
0

这显然是一个难题。除了明显需要小心之外,我认为第一步是准确定义您需要哪些线程以及为什么需要。

像设计类一样设计线程:确保知道是什么使它们保持一致:它们的内容以及它们与其他线程的交互。

于 2008-11-02T09:04:09.953 回答
0

我记得当我发现 Java 的 synchronizedList 类不是完全线程安全的,而只是有条件的线程安全时,我感到有些震惊。如果我没有将我的访问(迭代器、设置器等)包装在同步块中,我仍然可能会被烧毁。这意味着我可能已经向我的团队和我的管理层保证我的代码是线程安全的,但我可能错了。我可以确保线程安全的另一种方法是使用工具来分析代码并让它通过。STP、Actor 模型、Erlang 等是获得后一种保证形式的一些方法。能够可靠地确保程序的属性是/将是编程的一大步。

于 2008-11-03T01:56:23.407 回答
0

看起来你的 IOC 有点像 FBP :-) 如果 JavaFBP 代码可以得到像你这样精通编写线程安全代码艺术的人的彻底审查,那就太好了……它在 SourceForge 的 SVN 上。

于 2009-01-11T19:53:49.770 回答
0

一些专家认为您的问题的答案是完全避免线程,因为几乎不可能避免无法预料的问题。引用线程问题

我们开发了一个流程,其中包括代码成熟度评级系统(有四个级别,红色、黄色、绿色和蓝色)、设计审查、代码审查、夜间构建、回归测试和自动代码覆盖率指标。确保程序结构视图一致的内核部分是在 2000 年初编写的,设计审查为黄色,代码审查为绿色。审稿人包括并发专家,而不仅仅是没有经验的研究生(Christopher Hylands(现为 Brooks)、Bart Kienhuis、John Reekie 和 [Ed Lee] 都是审稿人)。我们编写了实现 100% 代码覆盖率的回归测试……系统本身开始被广泛使用,并且每次使用系统都会执行此代码。直到四年后的 2004 年 4 月 26 日代码陷入僵局,才发现任何问题。

于 2010-06-25T20:52:25.523 回答
0

使用多线程设计新应用程序的最安全方法是遵守以下规则:

没有设计低于设计。

这意味着什么?

想象一下,您确定了应用程序的主要构建块。让它成为 GUI,一些计算引擎。通常,一旦您拥有足够大的团队规模,团队中的一些人会要求“库”在这些主要构建块之间“共享代码”。虽然在开始时为主要构建块定义线程和协作规则相对容易,但所有这些努力现在都处于危险之中,因为“代码重用库”将设计得很糟糕,在需要时进行设计,并且到处都是锁和互斥锁“感觉不错”。这些临时库是您设计之下的设计,也是您的线程架构的主要风险。

该怎么办?

  • 告诉他们你宁愿重复代码而不是跨线程边界共享代码。
  • 如果您认为,该项目将真正受益于一些库,建立它们必须是无状态和可重入的规则。
  • 您的设计正在不断发展,其中一些“通用代码”可能会在设计中“向上移动”,成为您应用程序的新主要构建块。
  • 远离酷酷的网上图书馆狂热。一些第三方库确实可以为您节省大量时间。但也有一种趋势,即任何人都有自己的“最爱”,这几乎不是必需的。随着您添加的每个第三方库,您遇到线程问题的风险就会增加。

最后同样重要的是,考虑在您的主要构建块之间进行一些基于消息的交互;例如,参见经常提到的演员模型。

于 2015-01-20T13:03:11.433 回答