Why aren't the calls in main sequential?

2019-02-26 20:12发布

问题:

I was going through some simple examples on threading/synchronizing from a book that claims the use of synchronized will allow access to the method by one thread being called on the same instance. It does serialize as promised but it seems that about 9/10 times the third Caller created in the Synch main method below comes before the second. This code is the example code showing the issues without a synchronized method.

class CallMe {
    void call(String msg) {
        System.out.print("[" + msg);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("CallMe Interrupted");
        }
        System.out.println("]");
    }
}

class Caller implements Runnable {
    String msg;
    CallMe target;
    Thread t;

    public Caller (CallMe target, String msg) {
        this.target = target;
        this.msg = msg;
        t = new Thread(this);
        t.start();
    }

    @Override
    public void run() {
        target.call(msg);
    }
}

class Synch {
    public static void main(String args[]) {
        CallMe target = new CallMe();
        Caller c1 = new Caller(target, "Hello");
        Caller c2 = new Caller(target, "Synchronized");
        Caller c3 = new Caller(target, "World");

        try {       
            c1.t.join();
            c2.t.join();
            c3.t.join();        
        } catch (InterruptedException e) {
            System.out.println("Synch Interrupted");
        }
    }
}

The book shows two ways to deal with the issue, they are -
synchronized void call(String msg) {...} and
public void run() { synchronized (target) {...} }

It's clear that both options work because, as opposed to the original code, the bracketed words are consistent like...

[Hello]
[World] (about 90% of the time the calls are backwards)
[Synchronized](1/many have Synchronized as the first msg)

...there's no rhyme or reason to the original code. So I know it's "working" and can be seen directly by placing breakpoints on each of the Caller instantiations. It works every time, obviously to me, when I do.

Why is the third Caller consistently calling call before the second?

回答1:

Threads by definition run in parallel, and none is given precedence over any other.

Once the threads are all launched it is essentially random which will run first, in general the first one launched will have a slight "head start" but that head start is tiny compared to the overhead of launching threads etc.

A quirk of your particular environment just happens to be favoring one thread, the results may well vary on different systems and certainly shouldn't be relied on.

Incidentally this is bad practice for a number of reasons:

public Caller (CallMe target, String msg) {
    this.target = target;
    this.msg = msg;
    t = new Thread(this);
    t.start();
}

(You probably got a compiler warning in fact).

Much better is to provide a start method

public Caller start() {
    t.start();
    return this;
}

and then do

new Caller(target, msg).start();

This absolutely ensures that the Caller object is fully initialized and ready to go before the Thread starts processing it.



回答2:

Why is the third call consistently calling call before the second?

It's not doing so consistently - it's doing so about 90% of the time.

Basically, synchronization isn't guaranteed to be first-in, first-out... and there's no guarantee that the calls will even be made in the order you're expecting. Three new threads are being started in quick succession - there is no guarantee about which thread will actually start executing its code first.

Fundamentally if you want to impose ordering on parallel code, you need to do so explicitly. Synchronization doesn't provide ordering - it only provides exclusivity.



回答3:

It does serialize as promised but it seems that about 9/10 times the third Caller created in the Synch main method below comes before the second.

Be careful to understand the meaning of "serialize" in your sentence: it means that all the code sections protected by the same lock will never run in parallel; in other words, their execution will be serial.

What it doesn't mean is "execution of these code sections will occur in a strict, specified order". It will not.