47 votes

Comment prouver par programme que StringBuilder n'est pas threadsafe?

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

113voto

Andrew Tobilko Points 1283

Problème

Je crains que le test que vous avez écrit est incorrecte.

La principale exigence est de partager le même StringBuilder exemple entre les différents threads. Alors que vous êtes la création d'un StringBuilder objet 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();
    ...
}

Explication

Pour prouver l' StringBuilder classe n'est pas thread-safe, vous avez besoin d'écrire un essai où n threads (n > 1) l'ajout de certains trucs à la même instance simultanément.

Être conscient de la taille de tous les trucs que vous allez à ajouter, vous serez en mesure de comparer cette valeur avec le résultat de l' 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());

Car c'est pas thread-safe, vous pourriez avoir de la difficulté à obtenir le résultat.

Un ArrayIndexOutOfBoundsException peut être jeté en raison de l' char[] AbstractStringBuilder#value tableau et le mécanisme d'allocation qui n'a pas été conçu pour utiliser le multithreading.

Test

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

}

Résultats

AbstractStringBuilderTest.testStringBuilder: 
    10000 total, 165 error, 5988 failed, 3847 passed.

AbstractStringBuilderTest.testStringBuffer:
    10000 total, 10000 passed.

18voto

Eugene Points 6271

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 compte pour vous, c’est que tous les chiffres de la plage [0..10] n’ont pas été ajoutés à StringBuilder .

Par contre, si vous remplacez StringBuilder par StringBuffer , vous obtiendrez toujours 10 éléments dans cette mémoire tampon (mais hors service).

11voto

alxg2112 Points 223

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 par une méthode de 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.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X