简体   繁体   中英

Object Sharing in Simple Multi-threaded Program

Introduction

I have written a very simple program as an attempt to re-introduce myself to multi-threaded programming in JAVA. The objective of my program is derived from this rather neat set of articles, written by Jakob Jankov. For the program's original, unmodified version, consult the bottom of the linked article.

Jankov's program does not System.out.println the variables, so you cannot see what is happening. If you .print the resulting value you get the same results, every time (the program is thread safe); however, if you print some of the inner workings, the "inner behaviour" is different, each time.

I understand the issues involved in thread scheduling and the unpredictability of a thread's Running . I believe that may be a factor in the question I ask, below.

Program's Three Parts

The Main Class:

public class multiThreadTester {

    public static void main (String[] args) {

        // Counter object to be shared between two threads:
        Counter counter = new Counter();

        // Instantiation of Threads:
        Thread counterThread1 = new Thread(new CounterThread(counter), "counterThread1");
        Thread counterThread2 = new Thread(new CounterThread(counter), "counterThread2");

        counterThread1.start();
        counterThread2.start(); 
    }
}

The objective of the above class is simply to share an object. In this case, the threads share an object of type Counter :

Counter Class

public class Counter {

    long count = 0;

    // Adding a value to count data member:
    public synchronized void add (long value) {
        this.count += value;
    }

    public synchronized long getValue() {
        return count;
    }
}

The above is simply the definition of the Counter class, which includes only a primitive member of type long .

CounterThread Class

Below, is the CounterThread class, virtually unmodified from the code provided by Jankov. The only real difference (despite my implementing Runnable as opposed to extending Thread ) is the addition of System.out.println() . I added this to watch the inner-workings of the program.

public class CounterThread implements Runnable {

    protected Counter counter = null;

    public CounterThread(Counter aCounter) {
        this.counter = aCounter;
    }

    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("BEFORE add - " + Thread.currentThread().getName() + ": " + this.counter.getValue());
            counter.add(i);
            System.out.println("AFTER  add - " + Thread.currentThread().getName() + ": " + this.counter.getValue());
        }   
    }
}

Question

As you can see, the code is very simple. The above code's only purpose is to watch what happens as two threads share a thread-safe object.

My question comes as a result of the output of the program (which I have tried to condense, below). The output is hard to "get consistent" to demonstrate my question, as the spread of the difference (see below) can be quite great:

Here's the condensed output (trying to minimize what you look at):

AFTER  add - counterThread1: 0
BEFORE add - counterThread1: 0
AFTER  add - counterThread1: 1
BEFORE add - counterThread1: 1
AFTER  add - counterThread1: 3
BEFORE add - counterThread1: 3
AFTER  add - counterThread1: 6
BEFORE add - counterThread1: 6
AFTER  add - counterThread1: 10
BEFORE add - counterThread2: 0 // This BEFORE add statement is the source of my question

And one more output that better demonstrates:

BEFORE add - counterThread1: 0
AFTER  add - counterThread1: 0
BEFORE add - counterThread1: 0
AFTER  add - counterThread1: 1
BEFORE add - counterThread2: 0
AFTER  add - counterThread2: 1
BEFORE add - counterThread2: 1
AFTER  add - counterThread2: 2
BEFORE add - counterThread2: 2
AFTER  add - counterThread2: 4
BEFORE add - counterThread2: 4
AFTER  add - counterThread2: 7
BEFORE add - counterThread2: 7
AFTER  add - counterThread2: 11
BEFORE add - counterThread1: 1 // Here, counterThread1 still believes the value of Counter's counter is 1
AFTER  add - counterThread1: 13
BEFORE add - counterThread1: 13
AFTER  add - counterThread1: 16
BEFORE add - counterThread1: 16
AFTER  add - counterThread1: 20

My question(s):

Thread safety ensures the safe mutability of a variable, ie only one thread can access an object at a time. Doing this ensures that the "read" and "write" methods will behave, appropriately, only writing after a thread has released its lock (eliminating racing).

Why, despite the correct write behaviour, does counterThread2 "believe" Counter 's value ( not the iterator i ) to still be zero? What is happening in memory? Is this a matter of the thread containing it's own, local Counter object?

Or, more simply, after counterThread1 has updated the value, why does counterThread2 not see - in this case, System.out.println() - the correct value? Despite not seeing the value , the correct value is written to the object.

Why, despite the correct write behaviour, does counterThread2 "believe" Counter's value to still be zero?

The threads interleaved in such a way to cause this behaviour. Because the print statements are outside of the synchronised block, it is possible for a thread to read the counter value then pause due to is scheduling while the other thread increments multiple times. When the waiting thread finally resumes and enters the inc counter method, the value of the counter will have moved on quite a bit and will no longer match what was printed in the BEFORE log line.

As an example, I have modified your code to make it more evident that both threads are working on the same counter. First I have moved the print statements into the counter, then I added a unique thread label so that we can tell which thread was responsible for the increment and finally I only increment by one so that any jumps in the counter value will stand out more clearly.

public class Main {

    public static void main (String[] args) {

        // Counter object to be shared between two threads:
        Counter counter = new Counter();

        // Instantiation of Threads:
        Thread counterThread1 = new Thread(new CounterThread("A",counter), "counterThread1");
        Thread counterThread2 = new Thread(new CounterThread("B",counter), "counterThread2");

        counterThread1.start();
        counterThread2.start();
    }
}

 class Counter {

    long count = 0;

    // Adding a value to count data member:
    public synchronized void add (String label, long value) {
        System.out.println(label+ " BEFORE add - " + Thread.currentThread().getName() + ": " + this.count);
        this.count += value;
        System.out.println(label+ " AFTER add - " + Thread.currentThread().getName() + ": " + this.count);
    }

    public synchronized long getValue() {
        return count;
    }
}

class CounterThread implements Runnable {

    private String label;
    protected Counter counter = null;

    public CounterThread(String label, Counter aCounter) {
        this.label = label;
        this.counter = aCounter;
    }

    public void run() {
        for (int i = 0; i < 10; i++) {
            counter.add(label, 1);
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM