27

如果我创建目前仅在单个线程中使用的类,我是否应该使它们成为线程安全的,即使我目前不需要它?可能会发生,我后来在多个线程中使用这个类,那时我可能会遇到竞争条件,如果我没有首先使类线程安全,可能很难找到它们。或者我应该让这个类不是线程安全的,以获得更好的性能?但是过早的优化是邪恶的。

不同的问题:如果需要,我应该使我的类线程安全(如果在多个线程中使用,否则不是)还是应该优化这个问题然后需要(如果我看到同步占用了处理时间的重要部分)?

如果我选择两种方式中的一种,是否有减少缺点的方法?还是存在我应该使用的第三种可能性?

编辑:我给出了我想到这个问题的原因。在我们公司,我们编写了一个非常简单的用户管理,将数据写入属性文件。我在一个网络应用程序中使用它,经过一些工作后我得到了奇怪的错误,用户管理忘记了用户的属性(包括名称和密码)和角色。这很烦人,但不能始终如一地重现,所以我认为这是竞争条件。由于我同步了从磁盘读取和写入的所有方法,因此问题消失了。所以我想,如果我们一开始就编写了带同步的类,我可能可以避免所有的麻烦?

编辑 2:当我查看实用程序员的技巧时,我看到了技巧 #41:始终为并发设计。这并不是说所有代码都应该是线程安全的,而是说设计应该考虑到并发性。

4

13 回答 13

29

我曾经尝试让一切都成为线程安全的——然后我意识到“线程安全”的真正含义取决于使用情况。您通常无法预测这种用法,调用者无论如何必须采取行动才能以线程安全的方式使用它。

这些天来,我写的几乎所有东西都假设单线程,并将线程知识放在少数几个重要的地方。

话虽如此,我也(在适当的情况下)创建不可变类型,这些类型自然适用于多线程 - 并且通常更容易推理。

于 2008-10-24T16:53:13.287 回答
22

从数据开始。决定明确共享哪些数据并加以保护。如果可能的话,用数据封装锁定。使用预先存在的线程安全并发集合。

尽可能使用不可变对象。使属性最终,在构造函数中设置它们的值。如果您需要“更改”数据,请考虑返回一个新实例。不可变对象不需要锁定。

对于非共享或线程限制的对象,不要花时间使它们成为线程安全的。

记录代码中的期望。JCIP 注释是可用的最佳预定义选择。

于 2008-10-25T04:27:35.070 回答
6

遵循“尽可能简单,但不简单”的原则。如果没有要求,您不应该使它们成为线程安全的。这样做是投机性的,而且可能是不必要的。线程安全编程为您的类增加了更多的复杂性,并且可能由于同步任务而降低它们的性能。

除非明确声明一个对象是线程安全的,否则期望它不是。

于 2008-10-24T17:00:26.377 回答
4

我个人只会在需要时设计“线程安全”的类——本着仅在需要时优化的原则。Sun 似乎在单线程集合类的示例中采用了相同的方式。

但是,如果您决定改变,有一些好的原则可以帮助您:

  1. 最重要的是:在同步之前三思。我曾经有一位同事曾经同步过一些东西“以防万一——毕竟同步一定更好,对吧?” 这是错误的,并且是多个死锁错误的原因。
  2. 如果您的对象可以是不可变的,则使它们不可变。这不仅有助于线程化,还有助于将它们安全地用于集合中,作为 Maps 等的键
  3. 使您的对象尽可能简单。理想情况下,每个人都应该只做一项工作。如果您发现您可能想要同步对一半成员的访问,那么您可能应该将对象一分为二。
  4. 学习 java.util.concurrent 并尽可能使用它。在 99% 的情况下,他们的代码会比你的(或我的)更好、更快、更安全。
  5. 阅读Java 中的并发编程,非常棒!
于 2008-10-24T17:19:22.577 回答
3

顺便说一句:同步!=线程安全。即便如此,您可能不会同时修改数据,但您可能会同时读取它。因此,请记住 Java 内存模型,其中同步意味着使数据在所有线程中可靠可用,而不仅仅是保护它的并发修改。

是的,在我看来,线程安全必须从一开始就内置,如果您需要处理并发,它取决于应用程序逻辑。永远不要假设任何事情,即使您的测试似乎很好,比赛条件也是睡狗。

于 2009-03-12T02:54:09.800 回答
2

我发现JCIP注释对于声明哪些类是线程安全的非常有用。我的团队将我们的类注释为@ThreadSafe@NotThreadSafe@Immutable。这比阅读 Javadoc 要清楚得多,而且FindBugs也可以帮助我们发现违反 @Immutable 和@GuardedBy合同的情况。

于 2008-10-25T04:04:57.127 回答
1

您绝对应该知道哪些代码段将是多线程的,哪些不会。

如果不能将多线程区域集中到一个小的、可控的部分,你就不会成功。您的应用程序中的多线程部分需要仔细检查、全面分析、理解并适应多线程环境。

其余的则没有,因此使其成为线程安全的将是一种浪费。

例如,对于 swing GUI,Sun 刚刚决定不再使用多线程。

哦,如果有人使用你的类——这取决于他们确保如果它在一个线程部分中,那么让它成为线程安全的。

Sun 最初推出了线程安全集合(仅)。问题是,线程安全不能成为非线程安全的(出于性能目的)。所以现在他们推出了带有包装器的非线程安全版本,以使它们成为线程安全的。在大多数情况下,包装器是不必要的——假设除非你自己创建线程,否则你的类不必是线程安全的——但在 javadocs 中记录它。

于 2008-10-24T17:00:48.903 回答
1

如果我创建目前仅在单个线程中使用的类,我应该使它们成为线程安全的吗

线程使用的类本身不需要线程安全,程序作为一个整体是线程安全的。如果它们受到适当的同步保护,您可以在线程之间安全地共享非“线程安全”类的对象。因此,在这一点变得明显之前,没有必要让一个类本身成为线程安全的。

但是,多线程是程序中的基本(架构)选择。这并不是事后才添加的东西。所以你应该从一开始就知道哪些类需要是线程安全的。

于 2011-11-03T13:20:01.760 回答
1

这是我个人的做法:

  • 尽可能使对象和数据结构不可变。这通常是一种很好的做法,并且自动是线程安全的。问题解决了。
  • If you have to make an object mutable then normally don't bother trying to make it thread safe. The reasoning for this is simple: when you have mutable state then locking / control cannot be safely handled by a single class. Even if you synchronize all the methods, this doesn't guarantee thread safety. And if you add synchronisation to an object that only ever gets used in a single-threaded context, then you've just added unnecessary overhead. So you might as well leave it up to the caller / user to implement whatever locking system is necessary.
  • If you provide a higher level public API then implement whatever locking is required to make your API thread safe. For higher level functionality the overhead of thread safety is pretty trivial, and your users will definitely thank you. An API with complicated concurrency semantics that the users need to work around is not a good API!

This approach has served me well over time: you may need to make the occasional exception but on average it's a very good place to start!

于 2012-03-08T06:43:34.363 回答
0

如果您想了解 Sun 在 Java API 中所做的事情,可以查看集合类。许多常见的集合类不是线程安全的,但有线程安全的对应物。根据 Jon Skeet(见评论)的说法,许多 Java 类最初是线程安全的,但它们并没有使开发人员受益,因此一些类现在有两个版本——一个是线程安全的,另一个不是线程安全的。

我的建议是,除非您必须这样做,否则不要使代码成为线程安全的,因为线程安全涉及一些开销。我想这与优化属于同一类别-在必须这样做之前不要这样做。

于 2008-10-24T16:52:44.317 回答
0

分别设计要从多个线程中使用的类,并记录仅从单个线程中使用的其他类。

单线程的更容易使用。

分离多线程逻辑有助于使同步正确。

于 2008-10-24T16:53:08.170 回答
0

“总是”在软件开发中是一个非常危险的词……像这样的选择是“总是”情境的。

于 2008-10-24T17:13:27.927 回答
0

为了避免竞争条件,只锁定一个对象 - 冗长地阅读竞争条件的描述,你会发现交叉锁(竞争条件是用词不当 - 竞争在那里停止)总是两个 + 线程试图锁定的结果两个 + 对象。

使所有方法同步并进行测试 - 对于任何实际必须处理问题的现实世界应用程序同步是一个很小的成本。他们没有告诉你的是,整个事情确实锁定在 16 位指针表上......那时你是呃,......

只要保持你的汉堡翻转简历'最新。

于 2009-09-27T22:59:57.463 回答