Comment prouver par programme que StringBuilder
n'est pas threadsafe?
J'ai essayé ça, mais ça ne marche pas:
public class Threadsafe {
public static void main(String[] args) throws InterruptedException {
long startdate = System.currentTimeMillis();
MyThread1 mt1 = new MyThread1();
Thread t = new Thread(mt1);
MyThread2 mt2 = new MyThread2();
Thread t0 = new Thread(mt2);
t.start();
t0.start();
t.join();
t0.join();
long enddate = System.currentTimeMillis();
long time = enddate - startdate;
System.out.println(time);
}
String str = "aamir";
StringBuilder sb = new StringBuilder(str);
public void updateme() {
sb.deleteCharAt(2);
System.out.println(sb.toString());
}
public void displayme() {
sb.append("b");
System.out.println(sb.toString());
}
}
class MyThread1 implements Runnable {
Threadsafe sf = new Threadsafe();
public void run() {
sf.updateme();
}
}
class MyThread2 implements Runnable {
Threadsafe sf = new Threadsafe();
public void run() {
sf.displayme();
}
}
Je crains que le test que vous avez écrit soit incorrect.
La principale exigence est de partager la même instance StringBuilder
entre différents threads. Alors que vous créez un objet StringBuilder
pour chaque thread.
Le problème est qu'un new Threadsafe()
initialise un new StringBuilder()
:
class Threadsafe {
...
StringBuilder sb = new StringBuilder(str);
...
}
class MyThread1 implements Runnable {
Threadsafe sf = new Threadsafe();
...
}
class MyThread2 implements Runnable {
Threadsafe sf = new Threadsafe();
...
}
Pour prouver que la classe StringBuilder
n'est pas thread-safe, vous devez écrire un test où n
threads (n > 1
) Ajoutent des éléments au même instance simultanément.
Étant conscient de la taille de tout ce que vous allez ajouter, vous pourrez comparer cette valeur avec le résultat de builder.toString().length()
:
final long SIZE = 1000; // max stream size
final StringBuilder builder = Stream
.generate(() -> "a") // generate an infinite stream of "a"
.limit(SIZE) // make it finite
.parallel() // make it parallel
.reduce(new StringBuilder(), StringBuilder::append, (b1, b2) -> b1);
// put each element in the builder
Assert.assertEquals(SIZE, builder.toString().length());
Comme il ne s'agit en réalité pas de thread-safe, vous pouvez avoir du mal à obtenir le résultat.
Un ArrayIndexOutOfBoundsException
peut être levé à cause du tableau char[] AbstractStringBuilder#value
Et du mécanisme d'allocation qui n'a pas été conçu pour être utilisé en multithreading.
Voici mon JUnit 5 test qui couvre à la fois StringBuilder
et StringBuffer
:
public class AbstractStringBuilderTest {
@RepeatedTest(10000)
public void testStringBuilder() {
testAbstractStringBuilder(new StringBuilder(), StringBuilder::append);
}
@RepeatedTest(10000)
public void testStringBuffer() {
testAbstractStringBuilder(new StringBuffer(), StringBuffer::append);
}
private <T extends CharSequence> void testAbstractStringBuilder(T builder, BiFunction<T, ? super String, T> accumulator) {
final long SIZE = 1000;
final Supplier<String> GENERATOR = () -> "a";
final CharSequence sequence = Stream
.generate(GENERATOR)
.parallel()
.limit(SIZE)
.reduce(builder, accumulator, (b1, b2) -> b1);
Assertions.assertEquals(
SIZE * GENERATOR.get().length(), // expected
sequence.toString().length() // actual
);
}
}
AbstractStringBuilderTest.testStringBuilder:
10000 total, 165 error, 5988 failed, 3847 passed.
AbstractStringBuilderTest.testStringBuffer:
10000 total, 10000 passed.
Beaucoup plus simple:
StringBuilder sb = new StringBuilder();
IntStream.range(0, 10)
.parallel()
.peek(sb::append) // don't do this! just to prove a point...
.boxed()
.collect(Collectors.toList());
if (sb.toString().length() != 10) {
System.out.println(sb.toString());
}
Il n'y aura pas d'ordre des chiffres (ils ne seront pas 012...
et ainsi de suite), mais c'est quelque chose qui ne vous intéresse pas. Tout ce qui vous importe, c’est que non tous les chiffres de l’étendue [0..10]
où ajouté à StringBuilder
.
D'autre part, si vous remplacez StringBuilder
par StringBuffer
, vous obtiendrez toujours 10 éléments dans ce tampon (mais dans le désordre).
Considérons le test suivant.
import Java.util.ArrayList;
import Java.util.List;
import Java.util.concurrent.ExecutionException;
import Java.util.concurrent.ExecutorService;
import Java.util.concurrent.Executors;
import Java.util.concurrent.Future;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class NotThreadSafe {
private static final int CHARS_PER_THREAD = 1_000_000;
private static final int NUMBER_OF_THREADS = 4;
private StringBuilder builder;
@Before
public void setUp() {
builder = new StringBuilder();
}
@Test
public void testStringBuilder() throws ExecutionException, InterruptedException {
Runnable appender = () -> {
for (int i = 0; i < CHARS_PER_THREAD; i++) {
builder.append('A');
}
};
ExecutorService executorService = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < NUMBER_OF_THREADS; i++) {
futures.add(executorService.submit(appender));
}
for (Future<?> future : futures) {
future.get();
}
executorService.shutdown();
String builtString = builder.toString();
Assert.assertEquals(CHARS_PER_THREAD * NUMBER_OF_THREADS, builtString.length());
}
}
Ceci est destiné à prouver que StringBuilder
n'est pas thread-safe avec la méthode preuve par contradiction . Lorsqu'il est exécuté, il lève toujours une exception comme suit:
Java.util.concurrent.ExecutionException: Java.lang.ArrayIndexOutOfBoundsException: 73726
at Java.util.concurrent.FutureTask.report(FutureTask.Java:122)
at Java.util.concurrent.FutureTask.get(FutureTask.Java:192)
at NotThreadSafe.testStringBuilder(NotThreadSafe.Java:37)
at Sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at Sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.Java:62)
at Sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.Java:43)
at Java.lang.reflect.Method.invoke(Method.Java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.Java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.Java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.Java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.Java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.Java:26)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.Java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.Java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.Java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.Java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.Java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.Java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.Java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.Java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.Java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.Java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.Java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.Java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.Java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.Java:70)
Caused by: Java.lang.ArrayIndexOutOfBoundsException: 73726
at Java.lang.AbstractStringBuilder.append(AbstractStringBuilder.Java:650)
at Java.lang.StringBuilder.append(StringBuilder.Java:202)
at NotThreadSafe.lambda$testStringBuilder$0(NotThreadSafe.Java:28)
at Java.util.concurrent.Executors$RunnableAdapter.call(Executors.Java:511)
at Java.util.concurrent.FutureTask.run(FutureTask.Java:266)
at Java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.Java:1149)
at Java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.Java:624)
at Java.lang.Thread.run(Thread.Java:748)
Par conséquent, StringBuilder
est cassé lorsqu'il est utilisé par plusieurs threads.