0

In an effort to practice my rusty Java, I wanted to try a simple multi-threaded shared data example and I came across something that surprised me.

Basically we have a shared AtomicInteger counter between three threads that each take turns incrementing and printing the counter.

main

AtomicInteger counter = new AtomicInteger(0);

CounterThread ct1 = new CounterThread(counter, "A");
CounterThread ct2 = new CounterThread(counter, "B");
CounterThread ct3 = new CounterThread(counter, "C");

ct1.start();
ct2.start();
ct3.start();

CounterThread

public class CounterThread extends Thread
{   
    private AtomicInteger _count;
    private String _id;

    public CounterThread(AtomicInteger count, String id)
    {
        _count = count;
        _id = id;
    }

    public void run()
    {
        while(_count.get() < 1000)
        {
            System.out.println(_id + ": " + _count.incrementAndGet());
            Thread.yield();
        }
    }
}

I expected that when each thread executed Thread.yield(), that it would give over execution to another thread to increment _count like this:

A: 1
B: 2
C: 3
A: 4
...

Instead, I got output where A would increment _count 100 times, then pass it off to B. Sometimes all three threads would take turns consistently, but sometimes one thread would dominate for several increments.

Why doesn't Thread.yield() always yield processing over to another thread?

Gray
  • 115,027
  • 24
  • 293
  • 354
Cory Klein
  • 51,188
  • 43
  • 183
  • 243
  • 2
    `yield()` is a suggestion. – Sotirios Delimanolis Sep 11 '13 at 17:39
  • Threads don't work this way. THe work independently so you are going to see bursts of A:1 A:2 .... thread.yield() is a hint at best. – Gray Sep 11 '13 at 17:39
  • See my answer here: http://stackoverflow.com/questions/17816047/unwanted-output-in-multithreading/17816231#17816231 – Gray Sep 11 '13 at 17:40
  • @Gray I'm not trying to architect something where order of `Thread` execution is guaranteed, I was just confused when `Thread` behaved in a manner different from the [documentation](http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/Thread.html#yield()) "Causes the currently executing thread object to temporarily pause and allow other threads to execute." – Cory Klein Sep 11 '13 at 17:42
  • @CoryKlein The semantic trick is that "other threads" includes "the current thread". Or that "allow" doesn't mean "ensure". – millimoose Sep 11 '13 at 17:45
  • 1
    You will probably find that the calls to yield are ignored altogether, and that a thread is actually paused when one of its output buffers gets filled up. – Philip Sheard Sep 11 '13 at 17:47
  • 1
    Don't ignore the starting thread! ct1 may run several times before ct2.start() has finished... – Arne Burmeister Sep 11 '13 at 17:52
  • I've moved my comments to an answer @Cory. – Gray Sep 11 '13 at 17:53

2 Answers2

2

I expected that when each thread executed Thread.yield(), that it would give over execution to another thread to increment _count like this:

In threaded applications that are spinning, predicting the output is extremely hard. You would have to do a lot of work with locks and stuff to get perfect A:1 B:2 C:3 ... type output.

The problem is that everything is a race condition and unpredictable due to hardware, race-conditions, time-slicing randomness, and other factors. For example, when the first thread starts, it may run for a couple of millis before the next thread starts. There would be no one to yield() to. Also, even if it yields, maybe you are on a 4 processor box so there is no reason to pause any other threads at all.

Instead, I got output where A would increment _count 100 times, then pass it off to B. Sometimes all three threads would take turns consistently, but sometimes one thread would dominate for several increments.

Right, in general with this spinning loops, you see bursts of output from a single thread as it gets time slices. This is also confused by the fact that System.out.println(...) is synchronized which affects the timing as well. If it was not doing a synchronized operation, you would see even more bursty output.

Why doesn't Thread.yield() always yield processing over to another thread?

I very rarely use Thread.yield(). It is a hint to the scheduler at best and probably is ignored on some architectures. The idea that it "pauses" the thread is very misleading. It may cause the thread to be put back to the end of the run queue but there is no guarantee that there are any threads waiting so it may keep running as if the yield were removed.

See my answer here for more info : unwanted output in multithreading

Community
  • 1
  • 1
Gray
  • 115,027
  • 24
  • 293
  • 354
  • 1
    Aside: an easy way to get well-ordered output however would be to use something like a task queue architecture. I.e. enforce ordering using a construct designed to do so explicitly, instead of trying to do so indirectly. – millimoose Sep 11 '13 at 18:25
1

Let's read some javadoc, shall we?

A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint.

[...]

It is rarely appropriate to use this method. It may be useful for debugging or testing purposes, where it may help to reproduce bugs due to race conditions. It may also be useful when designing concurrency control constructs such as the ones in the java.util.concurrent.locks package.

You cannot guarantee that another thread will obtain the processor after a yield(). It's up to the scheduler and it seems he/she doesn't want to in your case. You might consider sleep()ing instead, for testing.

Community
  • 1
  • 1
Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
  • 1
    Ah, it appears part of my "rustiness" is that I am using [old documentation](http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/Thread.html#yield()). – Cory Klein Sep 11 '13 at 17:44
  • I bet the functionality was the same back then. That documentation seems ambiguous with respect to this issue. – Cory Klein Sep 11 '13 at 17:46
  • @SotiriosDelimanolis The docs could a holdover from when Java was using green threads - it might not have been updated thoroughly until later. – millimoose Sep 11 '13 at 18:22