如何确保实用程序静态方法的线程安全性?

94

有没有一般的方法或规则可用来确保任何应用程序中特定的各种工具类使用的静态方法的线程安全性?在这里我想特别指出Web应用程序的线程安全性。

众所周知,以不可变对象为参数的静态方法是线程安全的,而以可变对象为参数的静态方法则不是。

如果我有一个用于操作java.util.Date的实用方法,并且该方法接受java.util.Date的实例,则此方法将不是线程安全的。那么如何在不改变参数传递方式的情况下使其线程安全?

public class DateUtils {

    public static Date getNormalizeDate(Date date) {
        // some operations
    }   
}

这个类 javax.faces.context.FacesContext 是否可变?将此类的实例传递给静态实用程序方法是否线程安全?

可作为参数传递或不可作为参数传递的实例类列表可能很长;因此,在编写此类实用程序时我们应该注意哪些要点?


12
为什么这个问题会被踩和有一个关闭请求?这是一个错误的问题吗? - Tapas Bose
你考虑过将这个静态方法设为synchronized吗? - Andrew Logvinov
17
@AndrewLogvinov 是的,我有考虑过。但是我不想在不知道自己为什么这样做的情况下使一个方法同步化。在什么情况下我们应该将静态方法同步化? - Tapas Bose
3
@TapasBose 那个最后的评论问题可能会引发一两本书。我建议阅读《Java并发编程实践》。 - Duncan Jones
2
你需要同步访问可变变量的方法(静态和实例)。对于实用类,我认为没有太大必要,因为它们应该是无状态的。 - Andrew Logvinov
7个回答

99
众所周知,将不可变对象作为参数的静态方法是线程安全的,而可变对象则不是。
我对此提出异议。传递给方法的参数存储在堆栈中,这是一个每个线程的习惯用法。
如果您的参数是可变对象,例如Date,那么您需要确保其他线程在其他地方同时没有修改它。但这与您的方法的线程安全无关,是另一回事。
你发布的方法是线程安全的。 它不保持状态,只对其参数进行操作。
我强烈建议您阅读Java Concurrency in Practice或类似的专门讲解Java线程安全的书籍。 这是一个复杂的主题,无法通过几个StackOverflow答案得到恰当的解决。

@Duncan:你还推荐哪些关于多线程的书籍? - Gaurav
在educative.io上有Java课程中的多线程 https://www.educative.io/collection/5307417243942912/5707702298738688?authorName=C.%20H.%20Afzal - Shrikant Prabhu
7
通常来说,“IMO,读某本书”并不是一个有帮助的回答。为什么不详细回答具体问题,而是把整本书扔给别人呢? - MasterJoe
我不同意。前三段回答了主要问题。同时,@tapas-bose 似乎没有并发编程方面的专业知识。参考那本优秀的书是获得这种专业知识的一种方式。 - AlexHomeBrew

30

由于您的类不包含任何成员变量,因此您的方法是无状态的(仅使用本地变量和参数),因此是线程安全的。

调用它的代码可能不是线程安全的,但这是另一个讨论。例如,Date 不是线程安全的,如果调用代码读取了另一个线程编写的 Date,则必须在 Date 编写和读取代码中使用适当的同步。


17

根据JVM的结构,本地变量、方法参数和返回值本质上是“线程安全”的。但是如果你要设计你的类,实例变量和类变量只有在适当的情况下才能保证线程安全。更多内容请点击这里


15
我看到很多答案,但没有一个真正指出原因。
所以可以这样思考,每当创建一个线程时,它就会使用自己的堆栈(我猜在创建时堆栈的大小约为2MB)。因此,发生的任何执行实际上都是在此线程堆栈的上下文中发生的。 创建的任何变量都存活在堆中,但它们的引用与堆栈一起存在,例外情况是静态变量不存活于线程堆栈中。
您进行的任何函数调用实际上都被推送到线程堆栈上,无论是静态的还是非静态的。由于完整的方法被推入堆栈中,因此发生的任何变量创建都存活于堆栈中(再次除了静态变量),并且只能由一个线程访问。
因此,所有方法都是线程安全的,直到它们改变某些静态变量的状态。

8
这是我的理解方式:想象一个CampSite(这是一个静态方法)。作为露营者,我可以在我的背包里带入许多物品(这些是通过堆栈传递的参数)。CampSite为我提供了放置帐篷和炊具等物品的地方,但如果CampSite唯一的功能是允许我修改自己的对象,则它是线程安全的。CampSite甚至可以创造出空气中的东西(FirePit firepit = new FirePit();),这些东西也会被创建在堆栈上。
在任何时候,我都可以带着我的背包里的所有物品消失,其他露营者中的任何一个人都可以出现,并且做他们消失前所做的事情。在此CampSite中的不同线程将无法访问在其他线程中由CampSite创建的堆栈上的对象。
假设只有一个campStove(一个CampStove对象,而不是单独的实例)。如果我分享一个CampStove对象,则需要考虑多线程问题。我不想打开我的campStove,然后消失,之后另一个露营者关闭它 - 我将永远检查我的热狗是否熟透,但它永远不会熟透。您需要在某个地方进行同步...在CampStove类中,在调用CampSite的方法中或在CampSite本身中...但正如Duncan Jones所说,“那是另一回事”。请注意,即使我们在非静态CampSite对象的单独实例中露营,共享campStove也会存在相同的多线程问题。

8
我建议在方法开始时创建该(可变)对象的副本,使用副本代替原始参数。类似这样:
public static Date getNormalizeDate(Date date) {
    Date input = new Date(date.getTime());
    // ...
}

4
对于日期来说这听起来不错,但对于自定义对象来说可能行不通,特别是如果这是一个他无法编辑的第三方自定义对象。 - Saurabh Patil
1
如果在第三方情况下无法克隆 - 除了要求第三方使其类实例线程安全之外,您无法做太多事情。 - R Kaja Mohideen

4
我们将举几个例子来看静态方法是否线程安全。
例子1:
public static String concat (String st1, String str2) {
return str1 + str2
}

现在上面的方法是线程安全的。

现在我们来看一个不安全的例子。

例子2:

 public static void concat(StringBuilder result, StringBuilder sb, StringBuilder sb1) {
    result.append(sb);
    result.append(sb1);
    }

如果你看到两种方法都非常原始,但其中一个是线程安全的,另一个则不是。为什么?它们有什么区别?
工具类中的静态方法是否容易出现非线程安全问题?很多问题,对吧?
现在一切都取决于你如何实现方法和你在方法中使用哪种类型的对象。你是否使用线程安全的对象?这些对象/类是否可变?
如果你看Example 1,concat方法的参数是String类型的,它们是不可变的并按值传递,因此该方法完全线程安全。
现在在Example 2中,参数是StringBuilder类型的,它们是可变的,因此其他线程可以更改StringBuilder的值,这使得该方法潜在地不是线程安全的。
但这并不是完全正确的。如果您在本地变量中调用此实用程序方法,则永远不会遇到与线程安全相关的任何问题。因为每个线程都使用自己的本地变量副本,因此您永远不会遇到任何线程安全问题。但这超出了上述静态方法的范围。它取决于调用函数/程序。
现在,在实用程序类中使用静态方法是一种正常的做法。那么我们该如何避免它呢?如果您看Example 2,我正在修改第一个参数。现在,如果您想使此方法真正线程安全,则可以做一件简单的事情。要么使用不可变变量/对象,要么不要更改/修改任何方法参数。
在Example 2中,我们已经使用了可变的StringBuilder,所以您可以更改实现方式来使静态方法线程安全,如下所示:
public static String concat1(StringBuilder sb, StringBuilder sb1) {
StringBuilder result = new StringBuilder();
result.append(sb);
result.append(sb1);
return result.toString();
}

再次回归基础,永远记住,如果您正在使用不可变对象和局部变量,则与线程安全问题相差甚远。

来自文章(https://nikhilsidhaye.wordpress.com/2016/07/29/is-static-method-in-util-class-threadsafe/)感谢 Nikhil Sidhaye 提供这篇简单的文章。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接