StringBuilder изменен несколькими потоками

Вопрос, который я задаю, связан с Разница между StringBuilder и StringBuffer, но не то же самое. Я хочу посмотреть, что на самом деле происходит, если StringBuilder изменен двумя потоками одновременно.

Я написал следующие классы:

public class ThreadTester
{
    public static void main(String[] args) throws InterruptedException
    {
        Runnable threadJob = new MyRunnable();
        Thread myThread = new Thread(threadJob);
        myThread.start();

        for (int i = 0; i < 100; i++)
        {
            Thread.sleep(10);
            StringContainer.addToSb("a");
        }

        System.out.println("1: " + StringContainer.getSb());
        System.out.println("1 length: " + StringContainer.getSb().length());
    }
}

public class MyRunnable implements Runnable
{
    @Override
    public void run()
    {
        for (int i = 0; i < 100; i++)
        {
            try
            {
                Thread.sleep(10);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            StringContainer.addToSb("b");
        }

        System.out.println("2: " + StringContainer.getSb());
        System.out.println("2 length: " + StringContainer.getSb().length());
    }
}

public class StringContainer
{
    private static final StringBuffer sb = new StringBuffer();

    public static StringBuffer getSb()
    {
        return sb;
    }

    public static void addToSb(String s)
    {
        sb.append(s);
    }
}

Сначала я сохранил StringBuffer в StringContainer. Поскольку StringBuffer является потокобезопасным, за один раз к нему может присоединяться только один поток, поэтому вывод согласован - оба потока сообщают о длине буфера как 200, например:

1: abababababababababbaabababababababbaababababababababababababbabaabbababaabbaababababbababaabbababaabababbaabababbababababaababababababababbababaabbaababbaababababababbaababbababaababbabaabbababababaab
1 length: 200
2: abababababababababbaabababababababbaababababababababababababbabaabbababaabbaababababbababaabbababaabababbaabababbababababaababababababababbababaabbaababbaababababababbaababbababaababbabaabbababababaab
2 length: 200

или один из них сообщил 199, а другой 200, например:

2: abbabababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab
2 length: 199
1: abbababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababa
1 length: 200

Ключ состоит в том, что последний поток для завершения составляет 200.

Теперь я изменил StringContainer, чтобы иметь StringBuilder вместо StringBuffer i.e.

public class StringContainer
{
    private static final StringBuilder sb = new StringBuilder();

    public static StringBuilder getSb()
    {
        return sb;
    }

    public static void addToSb(String s)
    {
        sb.append(s);
    }
}

Я ожидаю, что некоторые из записей будут переписаны, что происходит. Но содержимое StringBuilder и длины иногда не совпадают:

1: ababbabababaababbaabbabababababaab
1 length: 137
2: ababbabababaababbaabbabababababaab
2 length: 137

Как вы можете видеть, печатный контент имеет только 34 символа, но длина равна 137. Почему это происходит?

@Extreme Coders - я сделал еще один тестовый прогон:

2: ababbabababaabbababaabbababaababaabbaababbaaababbaabbabbabbabababbabababbbabbbbbabababbaabababbabaabaaabaababbaabaababababbaabbbabbbbbababababbababaab
1: ababbabababaabbababaabbababaababaabbaababbaaababbaabbabbabbabababbabababbbabbbbbabababbaabababbabaabaaabaababbaabaababababbaabbbabbbbbababababbababaab
1 length: 150
2 length: 150

Версия Java: 1.6.0_45, и я использую версию eclipse: Eclipse Java EE IDE для веб-разработчиков. Версия: Juno Service Release 2 Идентификатор сборки: 20130225-0426

ОБНОВЛЕНИЕ 1: Я запустил это внешнее затмение, и теперь они кажутся подходящими, но иногда я получаю исключение ArrayIndexOutOfBoundsException:

$ java -version
java version "1.6.0_27"
OpenJDK Runtime Environment (IcedTea6 1.12.5) (6b27-1.12.5-0ubuntu0.12.04.1)
OpenJDK Server VM (build 20.0-b12, mixed mode)

$ java ThreadTester
1: ababbbbbabbabababababaababbaabbbaabababbbababbabababbabbababbbbbbabaabaababbbbbbabbbbbaabbaaabbbbaabbbababababbbbabbababab
1 length: 123
2: ababbbbbabbabababababaababbaabbbaabababbbababbabababbabbababbbbbbabaabaababbbbbbabbbbbaabbaaabbbbaabbbababababbbbabbababab
2 length: 123

$ java ThreadTester 
2: abbabaabbbbbbbbbababbbbbabbbabbbabaaabbbbbbbabababbbbbbbbbabbbbbbbababababbabbbbaabbbaaabbabaaababaaaabaabbaabbbb
2 length: 115
1: abbabaabbbbbbbbbababbbbbabbbabbbabaaabbbbbbbabababbbbbbbbbabbbbbbbababababbabbbbaabbbaaabbabaaababaaaabaabbaabbbb
1 length: 115

$ java ThreadTester 
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
    at java.lang.System.arraycopy(Native Method)
    at java.lang.String.getChars(String.java:862)
    at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:408)
    at java.lang.StringBuilder.append(StringBuilder.java:136)
    at StringContainer.addToSb(StringContainer.java:14)
    at ThreadTester.main(ThreadTester.java:14)
2: abbbbbbababbbbabbbbababbbbaabbabbbaaabbbababbbbabaabaabaabaaabababaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
2 length: 114

ArrayIndexOutOfBoundsException также происходит при запуске из eclipse.

ОБНОВЛЕНИЕ 2: Возникают две проблемы. Первая проблема с содержимым StringBuilder, не соответствующая длине, происходит только в Eclipse, а не при запуске в командной строке (по крайней мере, в 100 раз я запускал ее в командной строке, она никогда не происходила).

Вторая проблема с ArrayIndexOutOfBoundsException должна быть связана с внутренней реализацией класса StringBuilder, который хранит массив символов и делает Arrays.copyOf, когда он расширяет размер. Но по-прежнему меня беспокоит, как происходит запись, прежде чем размер будет расширен, независимо от порядка выполнения.

Кстати, я склонен согласиться с @GreyBeardedGeek ответить, что все это упражнение - огромная трата времени:-). Иногда мы видим только симптомы, то есть вывод какого-то кода, и задаемся вопросом, что происходит не так. Этот вопрос объявил a priori, что два потока изменяют (очень известный) поток небезопасного объекта.

ОБНОВЛЕНИЕ 3: Вот официальный ответ от Java Concurrency на практике. 35:

  • В отсутствие синхронизации компилятор, процессор и время выполнения может сделать некоторые совершенно странные вещи в порядке, в котором операции выполняются. Попытки обосновать порядок в какие действия памяти "должны" произойти в недостаточно синхронизированной многопоточные программы почти наверняка будут неверными.

  • Рассуждение о недостаточно синхронизированных параллельных программах непомерно трудно.

Существует также хороший пример NoVisibility в книге на стр. 34.

Ответ 1

Поведение класса non-threadsafe при доступе одновременно несколькими потоками по определению "undefined".

Любая попытка установить детерминированное поведение в таком случае - ИМХО,  просто огромная трата времени.

Ответ 2

Несоответствие между количеством напечатанных символов и напечатанной длиной происходит от печати значений, пока другой поток все еще работает. Ошибка связана с синхронизацией и вызвана тем, что оба потока пытаются изменить один и тот же объект одновременно.

Между первым и вторым println другой поток завершил дополнительный цикл и изменил содержимое буфера.