我应该始终使我的 java 代码线程安全,还是出于性能原因仅在需要时才这样做?
Posted
技术标签:
【中文标题】我应该始终使我的 java 代码线程安全,还是出于性能原因仅在需要时才这样做?【英文标题】:Should I always make my java-code thread-safe, or for performance-reasons do it only when needed? 【发布时间】:2010-09-19 01:25:47 【问题描述】:如果我创建目前仅在单个线程中使用的类,我是否应该使它们成为线程安全的,即使我目前不需要它?可能会发生,我后来在多个线程中使用这个类,那时我可能会遇到竞争条件,如果我没有首先使类线程安全,可能很难找到它们。或者我应该让这个类不是线程安全的,以获得更好的性能?但是过早的优化是邪恶的。
不同的问题:如果需要,我应该让我的类线程安全(如果在多个线程中使用,否则不是)还是应该优化这个问题然后需要(如果我看到同步占用了处理时间的重要部分) ?
如果我选择这两种方式中的一种,是否有减少缺点的方法?还是存在我应该使用的第三种可能性?
编辑:我给出了我想到这个问题的原因。在我们公司,我们编写了一个非常简单的用户管理,将数据写入属性文件。我在一个网络应用程序中使用它,经过一些工作后我得到了奇怪的错误,用户管理忘记了用户的属性(包括名称和密码)和角色。这很烦人,但不能始终如一地重现,所以我认为这是竞争条件。由于我同步了从磁盘读取和写入的所有方法,因此问题消失了。所以我想,如果我们一开始就编写了带同步的类,我可能可以避免所有的麻烦?
编辑 2:当我查看实用程序员的技巧时,我看到了技巧 #41:始终为并发设计。这并不是说所有代码都应该是线程安全的,而是说设计应该考虑到并发性。
【问题讨论】:
帮助您进行分析的方法是不要将并发视为过早的优化,而是类似于 BigO 的性能问题。过早的优化是邪恶的观察是指人们在 C/C++ 中展开循环或神秘的 switch-if 条件构造,这些构造无法破译和维护,并且在编译器完成代码后可能不会给您带来任何性能提升(或在 java 中运行时 JIT)。 【参考方案1】:我曾经尝试让一切都成为线程安全的——后来我意识到“线程安全”的真正含义取决于使用情况。您通常无法预测这种用法,调用者将必须采取措施以线程安全的方式使用它。
这些天来,我写的几乎所有东西都假设单线程,并将线程知识放在少数几个重要的地方。
话虽如此,我也(在适当的情况下)创建不可变类型,这些类型自然适用于多线程 - 并且总体上更易于推理。
【讨论】:
你有一个例子,在哪里使用会破坏一个类的线程安全? 非常容易 - 获取一个集合,其中每个操作本身都是线程安全的。现在迭代它 - 砰,线程安全消失了。您不希望每个单独的操作都取出一个锁 - 您在迭代的整个过程中都需要一个锁。集合本身无法为您做到这一点。 好的,有了这个解释,您的帖子值得一票。谢谢。 此外,一些幼稚的线程安全实现不会考虑内存模型,因此您最终会得到在单处理器上运行但不是线程的代码-当线程驻留在不同的处理器上时是安全的。 是的,vm 可以创建您不知道的线程,而且 jit 可以在运行时进行数千次迭代,从而导致测试中未披露的事件场景。可能典型的例子是最近我基于 java.util 中的警告编写类时它没有按照它所说的那样做,我必须在破译甚至没有“null”的异常之上实现完整复制语义 -它使用的是生成原始文件的类的名称并且犹豫不决,因为它是一个保存引用的系统线程。【参考方案2】:从数据开始。决定明确共享哪些数据并加以保护。如果可能的话,用数据封装锁定。使用预先存在的线程安全并发集合。
尽可能使用不可变对象。使属性最终,在构造函数中设置它们的值。如果您需要“更改”数据,请考虑返回一个新实例。不可变对象不需要锁定。
对于非共享或线程限制的对象,不要花时间使它们成为线程安全的。
在代码中记录期望。 JCIP 注释是可用的最佳预定义选择。
【讨论】:
我喜欢这个答案。您选择一种方式(不要过早同步),但您也提供了一些解决方案,这将有助于减少陷入问题的危险。【参考方案3】:遵循“尽可能简单,但不简单”的原则。如果没有要求,您不应该使它们成为线程安全的。这样做是投机性的,而且可能是不必要的。线程安全编程为您的类增加了更多的复杂性,并且可能由于同步任务而降低它们的性能。
除非明确声明一个对象是线程安全的,否则期望它不是。
【讨论】:
【参考方案4】:我个人只会在需要时设计“线程安全”的类——本着仅在需要时优化的原则。 Sun 似乎在单线程集合类的示例中采用了相同的方式。
但是,如果您决定改变,有一些好的原则可以帮助您:
-
最重要的是:在同步之前三思而后行。我曾经有一位同事曾经同步过一些东西“以防万一——毕竟同步一定更好,对吧?”这是错误的,是导致多个死锁错误的原因。
如果您的对象可以是不可变的,请将它们设置为不可变的。这不仅有助于线程化,还有助于将它们安全地用于集合中,作为 Map 的键等
让您的对象尽可能简单。理想情况下,每个人都应该只做一项工作。如果您发现您可能想要同步对一半成员的访问,那么您可能应该将对象一分为二。
学习 java.util.concurrent 并尽可能使用它。在 99% 的情况下,他们的代码会比你(或我的)更好、更快、更安全。
阅读Concurrent Programming in Java,太棒了!
【讨论】:
@Nick:我很想看到你关于同步导致多个死锁错误的评论,你会考虑做一个危险风格的问题,你会问一段同步的代码如何导致死锁并提供答案?听到这个消息会非常有用。 @Nick:由于线程不能对自身进行死锁,因此死锁的应用程序必须有多个线程访问相同的对象。因此,肯定需要一些同步。 你是对的,我并不是说同步不好——显然不是。我是说没有思考的同步是不好的。【参考方案5】:顺便说一句:同步!=线程安全。即便如此,您可能不会同时修改数据,但您可能会同时读取它。因此,请记住 Java 内存模型,其中同步意味着使数据在所有线程中可靠可用,而不仅仅是保护对它的并发修改。
是的,在我看来,线程安全必须从一开始就内置,如果您需要处理并发,这取决于应用程序逻辑。永远不要假设任何事情,即使你的测试看起来不错,比赛条件也是睡狗。
【讨论】:
【参考方案6】:我发现JCIP 注释对于声明哪些类是线程安全的非常有用。我的团队将我们的课程注释为@ThreadSafe、@NotThreadSafe 或@Immutable。这比阅读 Javadoc 要清楚得多,FindBugs 也可以帮助我们发现违反 @Immutable 和 @GuardedBy 合同的情况。
【讨论】:
【参考方案7】:您应该绝对知道哪些代码段将是多线程的,哪些不会。
如果不能将多线程区域集中到一个小的、可控的部分,你就不会成功。您的应用程序中的多线程部分需要仔细检查、全面分析、理解并适应多线程环境。
其余部分没有,因此使其线程安全将是一种浪费。
例如,对于 swing GUI,Sun 决定不再使用多线程。
哦,如果有人使用你的类——由他们来确保它是否在线程部分中,然后使其成为线程安全的。
Sun 最初推出了线程安全集合(仅)。问题是,线程安全不能成为非线程安全的(出于性能目的)。所以现在他们推出了带有包装器的非线程安全版本,以使它们成为线程安全的。在大多数情况下,包装器是不必要的——假设除非你自己创建线程,否则你的类不必是线程安全的——但在 javadocs 中记录它。
【讨论】:
【参考方案8】:如果我创建目前仅在单个线程中使用的类,我是否应该让它们成为线程安全的
线程使用的类不需要本身是线程安全的,程序作为一个整体是线程安全的。您可以在线程之间安全地共享非“线程安全”类的对象如果它们受到适当的同步保护。因此,在这一点变得明显之前,没有必要让类本身成为线程安全的。
但是,多线程是程序中的基本(架构)选择。它是not really something to add as an after thought。所以你应该从一开始就知道哪些类需要是线程安全的。
【讨论】:
【参考方案9】:这是我个人的做法:
尽可能使对象和数据结构不可变。这通常是一种很好的做法,并且自动是线程安全的。问题已解决。 如果您必须使对象可变,那么通常不要费心尝试使其线程安全。原因很简单:当您具有可变状态时,单个类无法安全地处理锁定/控制。即使您同步所有方法,也不能保证线程安全。而且,如果您将同步添加到仅在单线程上下文中使用的对象,那么您只是增加了不必要的开销。因此,您不妨将其留给调用者/用户来实施任何必要的锁定系统。 如果您提供更高级别的公共 API,则 实施任何锁定以确保您的 API 线程安全。对于更高级别的功能,线程安全的开销非常小,您的用户一定会感谢您。用户需要解决的具有复杂并发语义的 API 并不是一个好的 API!随着时间的推移,这种方法对我很有帮助:您可能需要偶尔例外,但平均而言,这是一个非常好的起点!
【讨论】:
【参考方案10】:如果您想了解 Sun 在 Java API 中所做的事情,可以查看集合类。许多常见的集合类不是线程安全的,但有线程安全的对应物。根据 Jon Skeet(参见 cmets)的说法,许多 Java 类最初是线程安全的,但它们并没有使开发人员受益,因此一些类现在有两个版本——一个是线程安全的,另一个不是线程安全的。
我的建议是,除非您必须这样做,否则不要使代码成为线程安全的,因为线程安全涉及一些开销。我想这与优化属于同一类别 - 在必须这样做之前不要这样做。
【讨论】:
实际上,Sun 的方法是相反的——最初的集合类(和 StringBuffer)是线程安全的,然后他们意识到这实际上并没有帮助任何人,所以当他们重新设计集合类时,他们使它们成为非线程安全的。 真的吗?我将更正我的帖子以确保历史记录是正确的。但我知道 Sun 这样做是出于性能原因,这是我的观点。谢谢。【参考方案11】:分别设计要在多个线程中使用的类,并记录其他仅在单个线程中使用的类。
单线程更容易使用。
分离多线程逻辑有助于使同步正确。
【讨论】:
【参考方案12】:“总是”在软件开发中是一个非常危险的词......像这样的选择是“总是”情境的。
【讨论】:
【参考方案13】:为了避免竞态条件,只锁定一个对象 - 冗长地阅读竞态条件的描述,你会发现交叉锁(竞态条件是用词不当 - 竞态在那里停止)总是两个 + 线程尝试的结果锁定两个 + 对象。
使所有方法同步并进行测试 - 对于实际必须处理问题的任何现实世界应用来说,同步的成本很低。他们没有告诉你的是,整个事情确实在 16 位指针表上锁定了......那时你是呃,......
只需保持您的汉堡翻转简历的最新状态。
【讨论】:
以上是关于我应该始终使我的 java 代码线程安全,还是出于性能原因仅在需要时才这样做?的主要内容,如果未能解决你的问题,请参考以下文章
如何在进行选择之前使我的代码中的复选框默认为未选中,同时保持我的功能相同?