“StringBuilders不是线程安全的”是什么意思?

16

我读过一些关于Java编程语言中StringStringBuilder的优缺点的文章。在其中一篇文章中,作者提到:

StringBuilder 不是线程安全的,在多线程使用时应该用 StringBuffer

不幸的是,我不明白这是什么意思。您能否解释在“线程安全”的上下文中,StringStringBuilderStringBuffer之间的区别呢?

如果可以,请给出代码示例。


请参考此链接:http://javahungry.blogspot.com/2013/06/difference-between-string-stringbuilder.html。 - Anptk
5个回答

18
如果多个线程同时修改同一个 StringBuilder 实例,则结果可能是意料之外的,即某些修改可能会丢失。因此,在这种情况下应该使用 StringBuffer。然而,如果每个线程只能由一个线程修改 StringBuilder 实例,最好使用 StringBuilder,因为它更高效(线程安全会带来性能成本)。

主要用例是顺序构建一个格式良好的字符串,使用StringBuilder/StringBuffer作为本地变量,而不涉及多线程共享。因此,在大多数情况下,StringBuilder是首选。 - Joop Eggen

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);
    }
}

如果许多线程调用addProperty方法,则结果会变得奇怪(不可预测的结果)。
Thread1: addProperty("a", "b");
Thread2: addProperty("c", "d");
Thread3: addProperty("e", "f");

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


3
即使您使用了 StringBuffer,您仍然可能会得到您提到的不可预测的结果。这可能是因为 Thread2 进行了 2 次追加,然后 Thread3 进行了 1 次追加,然后 Thread2 再进行了 2 次追加等等。这取决于 JVM 使用的线程管理策略。为了避免这种情况,您需要将整个方法 addProperty 同步。与 StringBuffer 的区别在于它不允许多个线程同时进入 append 方法本身(append 是同步的),从而覆盖彼此的更改 - 请参见下面 Stephen 的回答。 - Adam Burley
@AdamBurley 谢谢您的回复。但我不明白为什么您在我的答案中解释了StringBuffer。问题是关于为什么StringBuilder不是线程安全的,所以我解释了它为什么不是线程安全的,但我没有解释哪个是安全的或如何使其安全,因为实际上这不是问题。我在我的答案中没有解释任何关于StringBuffer的内容。所以对我来说,您的评论与我的答案无关。 - Jaya Ananthram
2
问题是关于StringBuilder不支持线程安全的含义。但是你回答中提到的问题与StringBuilder不支持线程安全无关。即使您使用了线程安全版本的StringBuilder(即StringBuffer,这就是我提到StringBuffer的原因),您仍将遇到您回答中提到的问题。导致您提到的问题的原因是调用StringBuilder的代码不是线程安全的,而不是StringBuilder本身不是线程安全的。 - Adam Burley

8

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;
 }

假设两个线程共享一个 StringBuilder 实例,并且同时尝试追加字符。假设它们同时到达 value[count++] = c; 语句,而且 count 的值为1。它们各自将字符写入缓冲区的 value[1],然后更新 count。显然只能存储一个字符……因此另一个字符将丢失。此外,count 的增量可能会丢失。
更糟糕的是,即使两个线程没有同时到达 value[count++] = c; 语句,该语句也可能失败。原因是 Java 内存模型表示,除非有适当的同步(或更准确地说是 happens-before 关系),否则不能保证第二个线程将看到第一个线程所做的内存更新。实际发生的情况取决于第一个线程的更新是否写入主存。
现在让我们看一下 StringBuffer.append(char):
public synchronized StringBuffer append(char c) {
    super.append(c);  // calls the "AbstractStringBuilder.append" method above.
    return this;
}

在这里,我们可以看到append方法是synchronized的。这意味着两件事:
  • 两个线程不能同时在同一个StringBuffer对象上执行超类的append方法。因此第一种情况不可能发生。

  • synchronize意味着不同线程对StringBuffer.append的连续调用之间存在happens before。这意味着后面的线程保证能够看到先前线程所做的更新。


String的情况又不同了。如果我们检查代码,我们会发现没有明显的同步机制。但这没关系,因为String对象实际上是不可变的;即String API中没有任何方法会导致String对象状态的外部可观察更改。此外:

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

  • String在幕后可变的唯一位置,无论线程是否看到hash变量的最新更改,hashCode()方法都将正常工作。


参考资料:


2
因为StringBuilder不是同步的,而StringBuffer是同步的。在多线程环境下使用StringBuilder时,多个线程可以同时访问StringBuilder对象,它产生的输出不能被预测,因此StringBuilder不是线程安全的...
使用StringBuffer,我们可以解决线程安全问题,因为StringBuffer是线程安全的,它是同步的,只有一个线程可以一次访问,因此可以预测和预计它产生的输出。

0

在方法内使用 StringBuilder 是安全的。

public void addProperty(String name, String value) {
    private StringBuilder sb = new StringBuilder("1=2");

        if (value != null && value.length() > 0) {
            if (sb.length() > 0) {
                sb.append(',');
        }
        sb.append(name).append('=').append(value);
    }
}

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