18.4 THREAD INTERFERENCE IN JAVA


18.4 THREAD INTERFERENCE IN JAVA

Multithreaded programming is simplest to implement when the various threads can operate independently, meaning when they share no data objects during their execution. That has been the case with all the programs we have shown so far. When the threads are given the ability to access and modify the same data, there is always a potential for a program to exhibit incorrect behavior.

In this section, we will present two examples of thread interference. The first example shows a subtle and insidious form of thread interference, caused by the threads interfering at the level of low-level instructions that the high-level Java statements get decomposed into. The second example will show thread interference for the special case of I/O.

Example 1

In this example, all the threads will share the same data object of the following type:

class DataObject {                   int dataItem1;                   int dataItem2;                   // ....          } 

Both the data members of the class, dataItem1 and dataItem2, will be set to 50 initially. When a thread is executed, its job will be to make equal and opposite modifications to the two data members so that their sum will always be equal to 100. So, as shown in lines (A2) and (A3) of the program below, we supply the class Data Object with a constructor where we initialize the two data members. We also supply the class with a method itemSwap in line (B1) for making equal and opposite modifications to the two data members in lines (B3) and (B5). Additional method for DataObject is the method test in line (C1) to test the sum of the values of dataItem1 and dataItem2 after the method itemSwap is invoked. You would expect test to always print out 100 on the console.

Note the invocation of keepBusy(10) inside itemSwap in line (B4). The keepBusy function tries to merely keep the processor busy for the designated number of milliseconds. The purpose of this function is to increase the length of the time the processor would need for completing itemSwap so as to increase the probability that there would be a timeslicing event at some point during the execution of itemSwap. As we will see later, it is this timeslicing action that is responsible for the thread interference we will show for this example.

For the purpose of invoking the method itemSwap repeatedly on an instance of type DataObject and to be able to do so in separate threads, we will define a class RepeatedSwaps in line (E) by subclassing from Thread. The run method of this multithreadable class, in line (G1), goes through 20,000 iterations and, via the statement in line (G4), we print out the sum of the two data members of the data object every 4000 iterations.

Finally, we define a class, UnsynchedSwaps in line (H) for creating different threads of type RepeatedSwaps, as shown in lines (J1) through (J5). Note that all the threads are supplied with the same data object, created in line (I). The source code follows:

 
//UnsynchedSwaps.java ///////////////////////// class DataObject ////////////////////////// class DataObject { int dataItem1; int dataItem2; DataObject() { //(A1) dataItem1 = 50; //(A2) dataItem2 = 50; //(A3) } void itemSwap() { //(B1) int x = (int) ( -4.999999 + Math.random() * 10 ); //(B2) dataItem1 -= x; //(B3) keepBusy(10); //(B4) dataItem2 += x; //(B5) } void test() { //(C1) int sum = dataItem1 + dataItem2; //(C2) System.out.println( sum ); //(C3) } public void keepBusy( int howLong ) { //(D) long curr = System.currentTimeMillis(); while ( System.currentTimeMillis() < curr + howLong ) ; } } //////////////////////// class RepeatedSwaps //////////////////////// class RepeatedSwaps extends Thread { //(E) DataObject dobj; RepeatedSwaps( DataObject d ) { //(F) dobj = d; start() } public void run() { //(G1) int i = 0; while ( i < 20000 ) { //(G2) dobj.itemSwap(); //(G3) if ( i % 4000 == 0 ) dobj.test(); //(G4) try { sleep( (int) (Math.random() * 2 ) ); } //(G5) catch( InterruptedException e ) {} i++; } } public void keepBusy() { long curr = System.currentTimeMillis(); while ( System.currentTimeMillis() < curr + (int) (Math.random()*;10) ) ; } } /////////////////////// class UnsynchedSwaps //////////////////////// public class UnsynchedSwaps { //(H) public static void main( String [] args ) { DataObject d = new DataObject(); //(I) new RepeatedSwaps( d ); //(J1) new RepeatedSwaps( d ); //(J2) new RepeatedSwaps( d ); //(J3) new RepeatedSwaps( d ); //(J4) } }

This program produces the following output:[8]

     100     98     100     100     105     98     100     99     103     94     98     91     100     97     98     96     100     104     98     105 

Evidently, something is wrong. Given that the method itemSwap() makes equal and opposite changes to the two data members, dataItem1 and dataItem2, the sum of the two should always add up to 100. So, obviously, it must the case that the threads are interfering with one another.

If you examine the Java code in the above program, it would seem that even if the threads interfered with one another, the sum of the values in dataItem1 and dataItem2 should always add up to 100. One would think that even if a thread, say thread1, was interrupted (on account of timeslicing) after executing the first data-modifying instructionof itemSwap:

     dataItem1 -= x; 

and some other thread, say thread2, went ahead executed both of the following instructions

     dataItem1 -= x;     dataItem2 += x; 

with possibly a different value of x, after thread1 woke up to execute its own

     dataItem2 += x; 

the overall result would still be the same, meaning the sum of the two data members dataItem1 and dataItem2 would still add up to 100.

But, unfortunately, it is not as simple as that, primarily because a single high-level instruction like

     dataItem2 += x; 

is decomposed in the Java byte code into more atomic operations of the form[9]

    1.  Load dataItem2 into a register    2.  Add x    3.  Move the result back to dataItem2 

Let's say thread1 is interrupted after it completes the first two steps. After thread1 is interrupted, let's say thread2 is able to complete all three steps. If thread1 wakes up soon after thread2 is done with step 3 and executes its own step 3, the result moved back to dataItem1 will not correspond to thread1 but to thread2.

So, basically, threads can corrupt the shared data any time they get interrupted in the middle of executing high-level source code instructions that access and modify the data.

Example 2

We will now show an example of thread interference for the case that involves file I/O. In the following program, the class DataFile is responsible for two things: (1) Its constructor creates a file named "hello.dat" and then deposits the string "Hello" in the file, as shown in line (B2). And (2) its method fileIO first tries to read the string that is in the file "hello.dat" and writes whatever was thus read back into the file; see lines (C2) and (C3). The class FileIO is the same as described in one of the homework problems in Chapter 10.

We next define in line (D) a multithreadable class ThreadedFileIO whose sole data member, defined in line (E), is an object of type DataFile. By invoking the fileIO method on this data member, we can do file I/O in each thread separately, but all the threads will of course be accessing the same file. The run method of this class in line (G) calls for four invocations of fileIO in each thread. After each invocation of fileIO, we print out the name of the thread and the string contained in the file.

Finally, we have in line (H) the class UnsynchedFileIO for constructing and launching five ThreadedFileIO threads in lines (J1) through (J5), all opening and closing the same file, "hello.dat". Note that each invocation, such as ThreadedFileIO("t0", dd) in line (J1), creates a new thread and makes it runnable automatically because the method start is embedded in the constructor of ThreadedFileIO. If we had not included this method there, our invocations would have to be of the form ThreadedFileIO("t0", dd).start().

Here is the source code for this example:

 
//UnsynchedFileIO.java /////////////////////////// class DataFile ////////////////////////// class DataFile { //(A) public DataFile () { //(B1) try { FileIO.writeOneString( "Hello", "hello.dat" ); //(B2) } catch( FileIOException e ) {} } void fileIO() { //(C1) try { String str = FileIO.readOneString( "hello.dat" ); //(C2) FileIO.writeOneString( str , "hello.dat"); //(C3) } catch( FileIOException e ) {} } } /////////////////////// class ThreadedFileIO //////////////////////// class ThreadedFileIO extends Thread { //(D) DataFile df; //(E) ThreadedFileIO( String threadName, DataFile d ) { //(F) df = d; setName( threadName ); start(); } public void run() { //(G) int i = 0; while ( i++ < 4 ) { try { df.fileIO(); String str = FileIO.readOneString( "hello.dat" ); System.out.printIn( getName() + ": " + "hello.dat contains: " + str ); sleep( 5 ); } catch( InterruptedException e ) {} catch( FileIOException e ) {} } } } /////////////////////// class UnsynchedFileIO /////////////////////// public class UnsynchedFileIO { //(H) public static void main( String[] args ) { DataFile dd = new DataFile(); //(I) new ThreadedFileIO( "t0", dd ); //(J1) new ThreadedFileIO( "t1", dd ); //(J2) new ThreadedFileIO( "t2", dd ); //(J3) new ThreadedFileIO( "t3", dd ); //(J4) new ThreadedFileIO( "t4", dd ); //(J5) } }

As with the previous example, the output of this program depends on which platform you run it on. The following output was produced by a state-of-the-art multiprocessor machine running Solaris:

     t1: hello.dat contains:     t2: hello.dat contains:     t0: hello.dat contains: Hello     t4: hello.dat contains: Hello     t3: hello.dat contains: Hello     t1: hello.dat contains: Hello     t2: hello.dat contains: Hello     t0: hello.dat contains: Hello     t3: hello.dat contains: Hello     t1: hello.dat contains: Hello     t0: hello.dat contains:     t2: hello.dat contains:     t4: hello.dat contains:     t3: hello.dat contains: Hello     t0: hello.dat contains: Hello     t1: hello.dat contains: Hello     t4: hello.dat contains:     t2: hello.dat contains: Hello     t3: hello.dat contains: Hello     t4: hello.dat contains: Hello 

Remember, each thread should make four appearances in the output. And for each thread the content of the file should ideally say "Hello." As is clear from the output, for some of the threads some of the time, the file appears to contain nothing. Evidently, the threads are getting in each other's way once again. The code of the method fileIO() of line (C1) where the threads can step on each other's toes consists of the following two statements in lines (C2) and (C3):

     String str = FileIO.readOneString( "hello.dat" );     FileIO.writeOneString( str ,  "hello.dat" ); 

The first statement reads the string that is contained in the file "hello.dat," and the second statement writes the same string back into the same file. But, as in the previous example, each of these high-level statements gets decomposed by the compiler into a number of low-level instructions. One of these low-level instructions for the write statement above erases the previous contents of the file before the file is written into.[10] Suppose one of the threads, call it t1, was interrupted immediately after "zeroing out" the file and suppose another thread, call it t2, was allowed to read this file next. The read method in t2 thread would return an empty string. Let's say that t2 is taken off the processor after this read step. Next, suppose the processor is assigned to t1 and it writes out the string "Hello" into the file. If t2 is given the processor soon thereafter, it could overwrite an empty string into the file. The behavior of the other threads subsequently would depend on whether they saw an empty file or the string "Hello" in the file. Also, after all the threads have run to completion, the file "hello.dat" may either contain the string "Hello" or it could be empty.

[8]This output was produced on a modern multiprocessor machine running the Solaris operating system. The output of this program will vary from platform to platform and from one run to another.

[9]To see this decomposition of high-level Java statements into the low-level instructions of the sort that would be susceptible to thread interference, you can "decompile" a previously compiled class by using the javap tool, as in

     javap -c -v DataObject 

This will show the bytecode in human readable form that the source code from the DataObject class got compiled into. The following segment of this bytecode is for the itemSwap method:

     Method void itemSwap()        0 ldc2_w #4 <Double -4.999999>        3 invokestatic #6 <Method double random()>        6 ldc2_ #7 <Double 10.0>        9 dmul       10 dadd       11 d2i       12 istore_1       13 aload_0       14 dup       15 getfield #2 <Field int dataItem1>       18 iload_1       19 isub       20 putfield #2 <Field int dataItem1>       23 aload_0       24 bipush 10       26 invokevirtual #9 <Method void keepBusy(int)>       29 aload_0       30 dup       31 getfield #3 <Field int dataItem2>       34 iload_1       35 iadd       36 putfield #3 <Field int dataItem2>       39 return 

[10]Earlier we said that threads block on I/O operations—that is, the processor cannot be wrested away from a thread until the thread has completed the I/O operation it is engaged in. The reader might now ask, How come the threads do not block on the I/O implied by readOneString and writeOneString? The answer is that a thread blocks during just the atomic / steps. A method such as writeOneString does many things besides data transfer: It has to either create a new file or open an existing file. In the latter case, it must also erase the contents of the file. Only then can the data be transfered to the file. A thread will block only during the data transfer phase.




Programming With Objects[c] A Comparative Presentation of Object-Oriented Programming With C++ and Java
Programming with Objects: A Comparative Presentation of Object Oriented Programming with C++ and Java
ISBN: 0471268526
EAN: 2147483647
Year: 2005
Pages: 273
Authors: Avinash Kak

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net