"StringBuilders不是线程安全"的含义

Ely*_*deh 8 java string stringbuilder multithreading stringbuffer

我已经阅读了一些关于Java程序设计语言中的Strings和StringBuilders的好处和缺点的文章.在其中一篇文章中,作者写道:String.

现在我无法理解它的含义.能否请你告诉我的差异StringBuilder,StringBuffer以及String特别是在"线程安全"的情况下.(用代码描述可能非常好,但如果不可能,任何提示将不胜感激).

Era*_*ran 14

如果多个线程正在修改a的相同实例StringBuilder,则结果可能是意外的 - 即某些修改可能会丢失.这就是为什么你应该在这种情况下使用StringBuffer.但是,如果每个线程StringBuilder实例只能由一个线程修改,则最好使用StringBuilder,因为它会更有效(线程安全带来性能成本).


Jay*_*ram 9

如果多个线程试图更改StringBuilder对象值,那么结果将是奇怪的.见下面的例子,

private StringBuilder sb = new StringBuilder("1=2");

public void addProperty(String name, String value) {
    if (value != null && value.length() > 0) {
        if (sb.length() > 0) {
            sb.append(',');
        }
        sb.append(name).append('=').append(value);
    }
}
Run Code Online (Sandbox Code Playgroud)

如果许多线程调用addProperty方法,那么结果将是奇怪的(不可预测的结果).

Thread1: addProperty("a", "b");
Thread2: addProperty("c", "d");
Thread3: addProperty("e", "f");
Run Code Online (Sandbox Code Playgroud)

最后,当你调用sb.toString()时,结果将是不可预测的.例如,它可能带来输出1=2,ac=d=b,e=f,但你的期望是1=2,a=b,c=d,e=f

  • 即使您使用“StringBuffer”,您仍然可能会得到您提到的不可预测的结果。可能会发生线程 2 进行 2 次追加,然后线程 3 进行 1 次追加,然后线程 2 进行 2 次追加等。取决于 JVM 使用的线程管理策略。为了避免这种情况,您必须使整个方法“addProperty”同步。与“StringBuffer”的区别在于它不允许多个线程位于“append”方法本身中(“append”是同步的),因此会覆盖彼此的更改 - 请参阅下面斯蒂芬的回答。 (3认同)
  • 问题是“StringBuilder”不是线程安全的意味着什么。但是您的答案中提到的问题与“StringBuilder”不是线程安全的无关。即使您使用了“StringBuilder”的线程安全版本(即“StringBuffer”,这就是我提到“StringBuffer”的原因),您仍然会遇到答案中提到的问题。导致您提到的问题的问题是您编写的调用“StringBuilder”的代码不是线程安全的,并且**不是**“StringBuilder”本身不是线程安全的。 (2认同)

Ste*_*n C 5

线程安全问题StringBuilder是方法调用StringBuilder不同步。

考虑该StringBuilder.append(char)方法的实现:

public StringBuilder append(boolean b) {
    super.append(b);
    return this;
}

// from the superclass
public AbstractStringBuilder append(char c) {
     int newCount = count + 1;
     if (newCount > value.length)
         expandCapacity(newCount);
     value[count++] = c;
     return this;
 }
Run Code Online (Sandbox Code Playgroud)

现在假设您有两个共享一个StringBuilder实例的线程,并且两个线程都尝试同时添加一个字符。假设他们都得到了value[count++] = c;发言的同时,那count1。每个人都会将其字符写入处的缓冲区value[1],然后进行更新count。显然,只能在其中存储一个字符...因此,另一个字符将丢失。此外,to的增量之一count可能会丢失。

更糟糕的是,value[count++] = c;即使两个线程没有同时到达那里,该语句也会失败。原因是Java内存模型说除非有适当的同步(“先发生”关系),否则不能保证第二个线程将看到第一个线程所做的内存更新。实际发生的情况取决于是否以及何时将第一个线程的更新写入主内存。


现在让我们看一下StringBuffer.append(char)

public synchronized StringBuffer append(char c) {
    super.append(c);  // calls the "AbstractStringBuilder.append" method above.
    return this;
}
Run Code Online (Sandbox Code Playgroud)

在这里,我们看到append方法是synchronized。这意味着两件事:

  • 两个线程不能append同时StringBuffer在同一个对象上执行超类方法。因此,第一种情况不会发生。

  • synchronize方法有一个happens before连续的调用之间StringBuffer.append通过不同的线程进行的。这意味着可以保证后面的线程可以看到前面的线程所做的更新。


String情况下,再次不同。如果我们检查代码,我们将看到没有明显的同步。但这没关系,因为String对象实际上是不可变的。即是在没有方法String的API,这将导致外部可见的改变String对象的状态。此外:

  • final实例变量和构造器的特殊行为意味着所有线程都会看到任何正确的初始状态String

  • 在一个在String后台可变的地方,hashCode()无论线程是否看到该hash变量的最新更改,该方法都能正常工作。


参考文献: