我只想在视觉上看到它们之间的区别,所以下面是代码。但它总是失败。有人可以帮我这个吗?我也见过关于SO的问题,但没有一个以编程方式显示出差异。
public class BBDifferencetest {
protected static int testnum = 0;
public static void testStringBuilder() {
final StringBuilder sb = new StringBuilder();
Thread t1 = new Thread() {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
testnum++;
sb.append(testnum);
sb.append(" ");
}
}
};
Thread t2 = new Thread() {
public void run() {
for (int x = 0; x < 100; x++) {
testnum++;
sb.append(testnum);
sb.append(" ");
}
}
};
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Result is: " + sb.toString());
}
public static void main(String args[]) {
testStringBuilder();
}
}
当我执行此操作时,我有时会以随机的方式获得输出,因此这证明了我的测试。但是,当我甚至用StringBuilder
替换StringBuffer
并进行测试时,即使它给了我意想不到的输出(而不是顺序,从1到200)。那么有人可以帮助我直观地了解它们之间的区别吗?
P.S:如果有人有您的代码显示差异,我会很乐意接受它作为答案。因为即使修改了代码,我也不确定是否可以与代码实现差异。
答案 0 :(得分:3)
(而不是顺序,从1到200)
每个线程都在testnum
上执行读取,修改和写入操作。 本身不是线程安全的。
然后每个线程再次获取testnum
的值以便附加它。另一个线程可能已被中断,并再次递增该值。
如果您将代码更改为:
AtomicInteger counter = new AtomicInteger();
...
sb.append(counter.getAndIncrement());
然后你更有可能看到你的期望。
为了更清楚,请将循环更改为仅调用append
一次,如下所示:
for (int x = 0; x < 100; x++) {
sb.append(counter.incrementAndGet() + " ");
}
当我这样做时,对于StringBuffer
我总是得到“完美”的输出。对于StringBuilder
我有时得到这样的输出:
97 98 100 102 104
这里两个线程同时附加了,内容已经搞砸了。
编辑:这是一个稍微简短的完整示例:
import java.util.concurrent.atomic.AtomicInteger;
public class Test {
public static void main(String[] args) throws InterruptedException {
final AtomicInteger counter = new AtomicInteger();
// Change to StringBuffer to see "working" output
final StringBuilder sb = new StringBuilder();
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
sb.append(counter.incrementAndGet() + " ");
}
}
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(sb);
}
}
答案 1 :(得分:1)
StringBuffer在方法级别同步。这意味着如果一个线程已经在他的一个方法中,没有人可以输入他的一个方法。但是它并不能保证只要另一个线程使用StringBuilder就会阻塞一个线程使用StringBuilder,因此这两个线程仍将争夺对方法的访问权限,并且你可能随机产生一个无序的结果。 / p>
真正锁定对StringBuffer的访问权限的唯一方法是将访问它的代码放在同步块中:
public void run() {
synchronized(sb) {
for (int x = 0; x < 100; x++) {
testnum++;
sb.append(testnum);
sb.append(" ");
}
}
}
如果你不这样做,那么线程1可以进入sb.append(testnum),线程2将在它的入口处等待,当线程1熄灭时,线程2可能会进入并开始在线程1进入sb.append(“”)之前写入。所以你会看到:
12 13 1415 16 ....
问题是,像这样的锁定也会使StringBuilder也适用。这就是为什么人们可以说StringBuffer上的同步机制是无用的,因此它不再使用的原因(Vector也是如此)。
所以,这样做不能告诉你StringBuilder和StringBuffer之间的区别。 Jon Skeet的回答更好。
答案 2 :(得分:0)
+1表示Cyrille所说的话。我想,只有固有原子类型数组(原语&lt; = 32位)的本质才能避免像使用StringBuilder一样获得ConcurrentModificationException,比如附加到List<Integer>
基本上,你有两个线程,每个100个单独的操作。两者在每次追加之前争夺对象的锁定,然后在每次追加之后释放100次。在每次迭代中获胜的线程将通过增加循环计数器和testnum所花费的(极少)时间来随机化。
与示例的不同之处不一定是排序,而是确保在使用StringBuilder时实际考虑所有插入。它没有内部同步,所以完全有可能在这个过程中有些人会被淹没或被覆盖。 StringBuffer将使用内部同步来处理这个问题,保证所有插入都能正确进行,但是您需要外部同步(如上面的Cyrille示例)来锁定每个线程的整个迭代序列以安全地使用StringBuilder。