StringBuilder modified by multiple threads
Asked Answered
C

2

10

The question I am asking is related to Difference between StringBuilder and StringBuffer but not the same. I want to see what really happens if a StringBuilder is modified by two threads at the same time.

I wrote the following classes:

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

Initially I kept a StringBuffer in the StringContainer. Since StringBuffer is thread-safe, at a time, only one thread can append to it, so the output is consistent - either both threads reported the length of the buffer as 200, like:

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

or one of them reported 199 and the other 200, like:

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

The key is that the last thread to complete reports a length of 200.

Now, I changed StringContainer to have a StringBuilder instead of 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);
    }
}

I expect some of the writes to be over-written, which is happening. But the contents of the StringBuilder and the lengths do not match sometimes:

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

As you can see the printed content has only 34 chars, but the length is 137. Why is this happening?

@Extreme Coders - I just did one more test run:

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

Java version: 1.6.0_45 and I am using eclipse version: Eclipse Java EE IDE for Web Developers. Version: Juno Service Release 2 Build id: 20130225-0426

UPDATE 1: I ran this outside eclipse and now they seem to be matching, but I am getting ArrayIndexOutOfBoundsException sometimes:

$ 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

The ArrayIndexOutOfBoundsException is also happening when running from eclipse.

UPDATE 2: There are two problems happening. The first problem of the contents of the StringBuilder not matching the length is happening only in Eclipse and not when I run in command line (at least the 100+ times I ran it on command line it never happened).

The second problem with ArrayIndexOutOfBoundsException should be to do with the internal implementation of StringBuilder class, which keeps an array of chars and does an Arrays.copyOf when it expands the size. But it still beats me how a write is happening before the size is expanded, no matter what the order of execution is.

BTW, I am inclined to agree with @GreyBeardedGeek's answer that this whole exercise is a huge waste of time :-). Sometimes we get to see only the symptoms i.e. the output of some code and wonder what is going wrong. This question declared a priori that two threads are modifying a (very well-known) thread unsafe object.

UPDATE 3: Here is the official answer from Java Concurrency in Practice p. 35:

  • In the absence of synchronization, the compiler, processor and runtime can do some downright weird things to the order in which operations appear to execute. Attempts to reason about the order in which memory actions "must" happen in insufficiently synchronized multithreaded programs will almost certainly be incorrect.

  • Reasoning about insufficiently synchronized concurrent programs is prohibitively difficult.

There is also a nice example NoVisibility in the book on p. 34.

Crump answered 8/6, 2013 at 19:44 Comment(5)
That's strange, on my machine it prints out 199 and 200 and the size of the StringBuilder is same as the number of the chars. I have ran this multiple times but each time getting the same resultsTillich
This also has something to do with the IDE. Initially I used IntelliJ and it always gave lengths of 199 and 200. Next I ran this in BlueJ and it gave variable length on every run. Seems like IntelliJ is better at handling multiple threads than other IDEs.Tillich
any chance of getting results from the command line IDE ? :)Stamp
@A.J. command-line IDE output added :)Crump
@ExtremeCoders: IntelliJ runs the same JVM as all the other IDEs. It can't be better than the other IDEs, since it doesn't run the program: the JVM does. And moreover nothing can be better in this area: the result is undetermined. The only way to have a correct result is to properly synchronize all the accesses to the StringBuilder.Pollak
G
8

The behavior of a non-threadsafe class when accessed concurrently by multiple threads is by definition "undefined".

Any attempt to ascertain deterministic behavior in such a case is, IMHO, just a huge waste of time.

Gelatinoid answered 8/6, 2013 at 20:19 Comment(1)
Nevertheless I think this case with printed length 137 and real length 50 is impossible even with "undefined" behaviour.Compliance
F
1

The discrepancy between the number of characters printed and the length printed comes from printing the values while the other thread is still running. The error is synchronization related and is caused by both threads trying to modify the same object at the same time.

Between the first and second println the other thread has completed an additional loop and changed the contents of the buffer.

Finnougrian answered 8/6, 2013 at 22:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.