Priorities and Scheduling

Chapter 7 - Concurrent Access to Objects and Variables

Java Thread Programming
Paul Hyde
  Copyright 1999 Sams Publishing

synchronized Method Modifier
The addition of the synchronized modifier to a method declaration ensures that only one thread is allowed inside the method at a time. This can be useful in keeping out other threads while the state of an object is temporarily inconsistent.
Two Threads Simultaneously in the Same Method of One Object
As BothInMethod (see Listing 7.4) shows, normally more than one thread is allowed to be inside a method at a time.
Listing 7.4  BothInMethod.javaShows That More Than One Thread Can Be Inside a Method
1: public class BothInMethod extends Object {
2:     private String objID;
3:
4:     public BothInMethod(String objID) {
5:         this.objID = objID;
6:     }
7:
8:     public void doStuff(int val) {
9:         print(entering doStuff());
10:         int num = val * 2 + objID.length();
11:         print(in doStuff() - local variable num= + num);
12:
13:         // slow things down to make observations
14:         try { Thread.sleep(2000); }
                       catch (InterruptedException x) { }
15:
16:         print(leaving doStuff());
17:     }
18:
19:     public void print(String msg) {
20:         threadPrint(objID= + objID + - + msg);
21:     }
22:
23:     public static void threadPrint(String msg) {
24:         String threadName = Thread.currentThread().getName();
25:         System.out.println(threadName + : + msg);
26:     }
27:
28:     public static void main(String[] args) {
29:         final BothInMethod bim = new BothInMethod(obj1);
30:
31:         Runnable runA = new Runnable() {
32:                 public void run() {
33:                     bim.doStuff(3);
34:                 }
35:             };
36:
37:         Thread threadA = new Thread(runA, threadA);
38:         threadA.start();
39:
40:         try { Thread.sleep(200); }
                       catch (InterruptedException x) { }
41:
42:         Runnable runB = new Runnable() {
43:                 public void run() {
44:                     bim.doStuff(7);
45:                 }
46:             };
47:
48:         Thread threadB = new Thread(runB, threadB);
49:         threadB.start();
50:     }
51: }
In main() (lines 2850), one BothInMethod object is instantiated with an identifier of obj1 (line 29). Next, two threads are created to simultaneously access the doStuff() method. The first is named threadA , and the second threadB . After threadA is started (line 38), it invokes doStuff() and passes in 3 (line 33). About 200 milliseconds later, threadB is started (line 49) and invokes doStuff() on the same object, passing in 7 (line 44).
Both threadA and threadB will be inside doStuff() (lines 817) at the same time; threadA will enter first, and threadB will follow 200 milliseconds later. Inside doStuff() , the local variable num is calculated using the int passed in as parameter val and the member variable objID (line 10). Because threadA and threadB each pass in a different val , the local variable num will be different for each thread. A sleep is used inside doStuff() to slow things down enough to prove that both threads are simultaneously inside the same method of the same object (line 14).
  Note If two or more threads are simultaneously inside a method, each thread has its own copy of local variables.
Listing 7.5 shows the output produced when BothInMethod is run. Your output should match. Notice that threadA enters doStuff() first (line 1) and calculates that its local variable num is 10 (line 2). While threadA is still inside doStuff() , threadB also enters it (line 3) and calculates that its local variable num is 18 (line 4). Next, threadA reports that it is leaving doStuff() (line 5) and is followed closely by threadB (line 6).
Listing 7.5  Output from BothInMethod (Your Output Should Match)
1: threadA: objID=obj1 - entering doStuff()
2: threadA: objID=obj1 - in doStuff() - local variable num=10
3: threadB: objID=obj1 - entering doStuff()
4: threadB: objID=obj1 - in doStuff() - local variable num=18
5: threadA: objID=obj1 - leaving doStuff()
6: threadB: objID=obj1 - leaving doStuff()
One Thread at a Time
More than one thread can be inside a method, and each thread keeps a copy of its own local variables. However, there are times when application constraints require that only one thread be permitted inside a method at a time. In OnlyOneInMethod (see Listing 7.6), the method modifier synchronized has been added to doStuff() (line 8). Other than this change, the rest of the class works the same as it did in BothInMethod .
Listing 7.6  OnlyOneInMethod.javaRestricting Access to One Thread at a Time
1: public class OnlyOneInMethod extends Object {
2:     private String objID;
3:
4:     public OnlyOneInMethod(String objID) {
5:         this.objID = objID;
6:     }
7:
8:     public synchronized void doStuff(int val) {
9:         print(entering doStuff());
10:         int num = val * 2 + objID.length();
11:         print(in doStuff() - local variable num= + num);
12:
13:         // slow things down to make observations
14:         try { Thread.sleep(2000); }
                       catch (InterruptedException x) { }
15:
16:         print(leaving doStuff());
17:     }
18:
19:     public void print(String msg) {
20:         threadPrint(objID= + objID + - + msg);
21:     }
22:
23:     public static void threadPrint(String msg) {
24:         String threadName = Thread.currentThread().getName();
25:         System.out.println(threadName + : + msg);
26:     }
27:
28:     public static void main(String[] args) {
29:         final OnlyOneInMethod ooim = new OnlyOneInMethod(obj1);
30:
31:         Runnable runA = new Runnable() {
32:                 public void run() {
33:                     ooim.doStuff(3);
34:                 }
35:             };
36:
37:         Thread threadA = new Thread(runA, threadA);
38:         threadA.start();
39:
40:         try { Thread.sleep(200); }
                       catch (InterruptedException x) { }
41:
42:         Runnable runB = new Runnable() {
43:                 public void run() {
44:                     ooim.doStuff(7);
45:                 }
46:             };
47:
48:         Thread threadB = new Thread(runB, threadB);
49:         threadB.start();
50:     }
51: }
Only one thread at a time will be allowed into doStuff() because it is now synchronized . Listing 7.7 shows the output when OnlyOneInMethod is run. Your output should match. Notice that this time, threadA enters (line 1) and leaves (line 3) the doStuff() method before threadB is allowed to enter (line 4). The addition of the synchronized modifier successfully guarded doStuff() and allowed only one thread to enter at a time.
Listing 7.7  Output from OnlyOneInMethod (Your Output Should Match)
1: threadA: objID=obj1 - entering doStuff()
2: threadA: objID=obj1 - in doStuff() - local variable num=10
3: threadA: objID=obj1 - leaving doStuff()
4: threadB: objID=obj1 - entering doStuff()
5: threadB: objID=obj1 - in doStuff() - local variable num=18
6: threadB: objID=obj1 - leaving doStuff()
When a thread encounters a synchronized instance method, it blocks until it can get exclusive access to the object-level mutex lock. Mutex is short for mutual exclusion. A mutex lock can be held by only one thread at a time. Other threads waiting for the lock will block until it is released. When the lock is released, all the threads waiting for it compete for exclusive access. Only one will be successful, and the other threads will go back into a blocked state waiting for the lock to be released again.
If one synchronized method on an object invokes another synchronized method on that same object, it will not block trying to get the object-level lock because it already has exclusive access to the lock.
Two Threads, Two Objects
Every instance of a class has its own object-level lock. The TwoObjects class shown in Listing 7.8 demonstrates that each object has its own object-level lock.
Listing 7.8  TwoObjects.java
1: public class TwoObjects extends Object {
2:     private String objID;
3:
4:     public TwoObjects(String objID) {
5:         this.objID = objID;
6:     }
7:
8:     public synchronized void doStuff(int val) {
9:         print(entering doStuff());
10:         int num = val * 2 + objID.length();
11:         print(in doStuff() - local variable num= + num);
12:
13:         // slow things down to make observations
14:         try { Thread.sleep(2000); }
                       catch (InterruptedException x) { }
15:
16:         print(leaving doStuff());
17:     }
18:
19:     public void print(String msg) {
20:         threadPrint(objID= + objID + - + msg);
21:     }
22:
23:     public static void threadPrint(String msg) {
24:         String threadName = Thread.currentThread().getName();
25:         System.out.println(threadName + : + msg);
26:     }
27:
28:     public static void main(String[] args) {
29:         final TwoObjects obj1 = new TwoObjects(obj1);
30:         final TwoObjects obj2 = new TwoObjects(obj2);
31:
32:         Runnable runA = new Runnable() {
33:                 public void run() {
34:                     obj1.doStuff(3);
35:                 }
36:             };
37:
38:         Thread threadA = new Thread(runA, threadA);
39:         threadA.start();
40:
41:         try { Thread.sleep(200); }
                       catch (InterruptedException x) { }
42:
43:         Runnable runB = new Runnable() {
44:                 public void run() {
45:                     obj2.doStuff(7);
46:                 }
47:             };
48:
49:         Thread threadB = new Thread(runB, threadB);
50:         threadB.start();
51:     }
52: }
This time, two different objects are created (lines 2930). The doStuff() method of obj1 is invoked by threadA (line 34). A fraction of a second later, the doStuff() method of obj2 is invoked by threadB (line 45). The doStuff() method is synchronized (line 8), but this time there is no competition between threadA and threadB for exclusive access. Each thread gets exclusive access to the object-level lock of the instance it is working on. Listing 7.9 shows the output when TwoObjects is run. Your output should match.
Listing 7.9 Output from TwoObjects (Your Output Should Match)
1: threadA: objID=obj1 - entering doStuff()
2: threadA: objID=obj1 - in doStuff() - local variable num=10
3: threadB: objID=obj2 - entering doStuff()
4: threadB: objID=obj2 - in doStuff() - local variable num=18
5: threadA: objID=obj1 - leaving doStuff()
6: threadB: objID=obj2 - leaving doStuff()
Although the doStuff() method is synchronized , there is no competition for exclusive access to the object-level lock. Each instance, obj1 and obj2 , has its own object-level lock. When threadA enters the doStuff() method of obj1 (line 1), it acquires exclusive access to the object-level lock for obj1 . When threadB enters the doStuff() method of obj2 (line 3), it acquires exclusive access to the object-level lock for obj2 .
Avoiding Accidental Corruption of an Object
CorruptWrite , shown in Listing 7.10, demonstrates the need to control concurrent access to a method. In this example, two strings are passed to a method for assignment into member variables. The trouble is that two threads are simultaneously trying to make assignments, and their assignments might get interleaved.
Listing 7.10  CorruptWrite.javaTrouble Because of the Lack of Synchronizatio n
1: public class CorruptWrite extends Object {
2:     private String fname;
3:     private String lname;
4:
5:     public void setNames(String firstName, String lastName) {
6:         print(entering setNames());
7:         fname = firstName;
8:
9:         // A thread might be swapped out here, and may stay
10:         // out for a varying amount of time. The different
11:         // sleep times exaggerate this.
12:         if (fname.length() < 5) {
13:             try { Thread.sleep(1000); }
14:             catch (InterruptedException x) { }
15:         } else {
16:             try { Thread.sleep(2000); }
17:             catch (InterruptedException x) { }
18:         }
19:
20:         lname = lastName;
21:
22:         print(leaving setNames() - + lname + , + fname);
23:     }
24:
25:     public static void print(String msg) {
26:         String threadName = Thread.currentThread().getName();
27:         System.out.println(threadName + : + msg);
28:     }
29:
30:     public static void main(String[] args) {
31:         final CorruptWrite cw = new CorruptWrite();
32:
33:         Runnable runA = new Runnable() {
34:                 public void run() {
35:                     cw.setNames(George, Washington);
36:                 }
37:             };
38:
39:         Thread threadA = new Thread(runA, threadA);
40:         threadA.start();
41:
42:         try { Thread.sleep(200); }
43:         catch (InterruptedException x) { }
44:
45:         Runnable runB = new Runnable() {
46:                 public void run() {
47:                     cw.setNames(Abe, Lincoln);
48:                 }
49:             };
50:
51:         Thread threadB = new Thread(runB, threadB);
52:         threadB.start();
53:     }
54: }
In main() (lines 3053), a single instance of CorruptWrite is created and referred to by cw (line 31). Two threads are started and both call the setNames() method passing in a first and last name . First, threadA executes cw.setNames(George, Washington) (line 35) and 200 milliseconds later, threadB executes cw.setNames(Abe, Lincoln) (line 47).
CorruptWrite has two member variables, fname and lname (lines 23), that hold the names passed to setNames() . Inside setNames() (lines 523), the first parameter passed is assigned to fname (line 7). Then, depending on the length of fname , the thread sleeps for either one or two seconds (lines 1218). After the nap, the second parameter is assigned to the member variable lname (line 20).
I used the variable length sleep inside setNames() to exaggerate what might really happen. The exaggeration will help to land the object in an inconsistent state. Real-world code would look more like the following:
public void setNames(String firstName, String lastName) {
    fname = firstName;
    lname = lastName;
}
This revised and quick setNames() method is still subtly dangerous in a multithreaded environment. It is possible that threadA could be swapped off the processor by the thread scheduler just after making the fname assignment, and just before making the lname assignment. Although threadA is halfway complete with its work, threadB could come along and assign its parameters to both fname and lname . When threadA gets scheduled to run again, it finishes up by assigning its second parameter to lname . This leaves the object in an inconsistent state. Most of the time this code will run fine, but now and then it will corrupt the object. By adding the variable-length sleeps in the setNames() method used in CorruptWrite , I have guaranteed that the object will wind up corrupted for the purposes of demonstration. Table 7.2 summarizes the states of the object at various points in time.
Table 7.2 States of the Member Variables of CorruptWrite at Various Points in Time
fname
lname
Point in Time
null
null
Before either thread enters setNames()
George
null
After threadA sets fname
Abe
Lincoln
After threadB sets both
Abe
Washington
After threadA sets lname
Listing 7.11 shows the output produced when CorruptWrite is run. Your output should match. Before going to sleep, threadA reports that it has entered the setNames() method (line 1). While threadA is sleeping, threadB comes along and enters setNames() (line 2). It assigns values to both member variables and just before returning, prints their values (line 3). When threadA wakes up, it assigns its second value and just before returning, prints the current values of the member variables (line 4) showing the inconsistent state. The name-pair Abe and Washington was never passed to setNames() , but this is the current (corrupted) state of the object.
Listing 7.11  Output from CorruptWrite (Your Output Should Match)
1: threadA: entering setNames()
2: threadB: entering setNames()
3: threadB: leaving setNames() - Lincoln, Abe
4: threadA: leaving setNames() - Washington, Abe
FixedWrite (see Listing 7.12) corrects the dangerous code in CorruptWrite simply by adding the synchronized method modifier to the setNames() method (line 5). Otherwise , FixedWrite is basically the same as CorruptWrite .
Listing 7.12  FixedWrite.javaUsing synchronized to Control Concurrent Changes
1: public class FixedWrite extends Object {
2:     private String fname;
3:     private String lname;
4:
5:     public synchronized void setNames(
6:                 String firstName,
7:                 String lastName
8:            ) {
9:
10:         print(entering setNames());
11:         fname = firstName;
12:
13:         // A thread might be swapped out here, and may stay
14:         // out for a varying amount of time. The different
15:         // sleep times exaggerate this.
16:         if (fname.length() < 5) {
17:             try { Thread.sleep(1000); }
18:             catch (InterruptedException x) { }
19:         } else {
20:             try { Thread.sleep(2000); }
21:             catch (InterruptedException x) { }
22:         }
23:
24:         lname = lastName;
25:
26:         print(leaving setNames() - + lname + , + fname);
27:     }
28:
29:     public static void print(String msg) {
30:         String threadName = Thread.currentThread().getName();
31:         System.out.println(threadName + : + msg);
32:     }
33:
34:     public static void main(String[] args) {
35:         final FixedWrite fw = new FixedWrite();
36:
37:         Runnable runA = new Runnable() {
38:                 public void run() {
39:                     fw.setNames(George, Washington);
40:                 }
41:             };
42:
43:         Thread threadA = new Thread(runA, threadA);
44:         threadA.start();
45:
46:         try { Thread.sleep(200); }
47:         catch (InterruptedException x) { }
48:
49:         Runnable runB = new Runnable() {
50:                 public void run() {
51:                     fw.setNames(Abe, Lincoln);
52:                 }
53:             };
54:
55:         Thread threadB = new Thread(runB, threadB);
56:         threadB.start();
57:     }
58: }
If you make setNames() a sychronized method, each thread that tries to enter this method will block until it can get exclusive access to the object-level lock. Now when threadA goes to sleep (or if the thread scheduler had otherwise decided to swap it out), threadB is prevented from entering the method. When threadA finally completes its work inside setNames() , it automatically releases the lock as it returns. Now, threadB is able to gain exclusive access to the lock and enters setNames() .
The variable-length sleeps are still inside setNames() to demonstrate that the addition of the synchronized method modifier solved the problem. Real-world code (that is truly safe) would look more like the following:
public synchronized void setNames(String firstName, String lastName) {
    fname = firstName;
    lname = lastName;
}
Listing 7.13 shows the output when FixedWrite is run. Your output should match. Notice that threadB doesnt enter setNames() (line 3) until threadA has left it (line 2). In this fixed version, both threadA and threadB print consistent name-pairs when they leave (lines 2, 4).
Listing 7.13  Output from FixedWrite (Your Output Should Match)
1: threadA: entering setNames()
2: threadA: leaving setNames() - Washington, George
3: threadB: entering setNames()
4: threadB: leaving setNames() - Lincoln, Abe
Deferring Access to an Object While It Is Inconsistent
FixedWrite solved the problem by ensuring that the object was left in a consistent state by a call to setNames() . The object is consistent both before and after a thread invokes setNames() , but it is inconsistent while a thread is inside setNames() . If one thread is executing setNames() at the same time that a second thread is accessing the member variables, the second thread might occasionally see an inconsistent name-pair. DirtyRead (see Listing 7.14) demonstrates this problem.
Listing 7.14  DirtyRead.javaAccessing Members Variables While in an Inconsistent State
1: public class DirtyRead extends Object {
2:     private String fname;
3:     private String lname;
4:
5:     public String getNames() {
6:         return lname + , + fname;
7:     }
8:
9:     public synchronized void setNames(
10:                 String firstName,
11:                 String lastName
12:            ) {
13:
14:         print(entering setNames());
15:         fname = firstName;
16:
17:         try { Thread.sleep(1000); }
18:         catch (InterruptedException x) { }
19:
20:         lname = lastName;
21:         print(leaving setNames() - + lname + , + fname);
22:     }
23:
24:     public static void print(String msg) {
25:         String threadName = Thread.currentThread().getName();
26:         System.out.println(threadName + : + msg);
27:     }
28:
29:     public static void main(String[] args) {
30:         final DirtyRead dr = new DirtyRead();
31:         dr.setNames(George, Washington) ; // initially
32:
33:         Runnable runA = new Runnable() {
34:                 public void run() {
35:                     dr.setNames(Abe, Lincoln);
36:                 }
37:             };
38:
39:         Thread threadA = new Thread(runA, threadA);
40:         threadA.start();
41:
42:         try { Thread.sleep(200); }
43:         catch (InterruptedException x) { }
44:
45:         Runnable runB = new Runnable() {
46:                 public void run() {
47:                     print(getNames()= + dr.getNames() );
48:                 }
49:             };
50:
51:         Thread threadB = new Thread(runB, threadB);
52:         threadB.start();
53:     }
54: }
DirtyRead is an evolution of FixedWrite . A new method called getNames() has been added (lines 57). This method constructs and returns a new String that is a combination of the lname and fname member variables (line 6).
The setNames() method (lines 922) is still synchronized (line 9). It has been simplified to just sleep for one second between the setting of the first name and last name.
Inside main() (lines 2953), a few different actions are taken. First, just after the DirtyRead instance is created (line 30), setNames() is invoked by the main thread to initially set the names to George and Washington (line 31). After that, threadA invokes setNames() , passing in Abe and Lincoln (line 35). threadA runs for about 200 milliseconds before threadB is started (line 42).
The setNames() method is still slow, so while the names are in the process of being changed by threadA , threadB invokes the getNames() method (line 47). Listing 7.15 shows the output when DirtyRead is run. Your output should match.
Listing 7.15  Output from DirtyRead (Your Output Should Match)
1: main: entering setNames()
2: main: leaving setNames() - Washington, George
3: threadA: entering setNames()
4: threadB: getNames()=Washington, Abe
5: threadA: leaving setNames() - Lincoln, Abe
The setNames() method is invoked by threadA (line 3). Before it can finish setting both names, threadB invokes getNames() (line 4). It returns the combination of Abe and Washington, catching the object in an inconsistent state. When threadA finishes executing setNames() , the object is back to a consistent state (line 5).
Its an unavoidable fact that the object must be in an inconsistent state for a brief period of time, even with everything except the assignments taken out:
public synchronized void setNames(String firstName, String lastName) {
    fname = firstName;
    lname = lastName;
}
No matter how fast the processor is, its possible that the thread scheduler could swap out the thread making the changes after it has changed fname but before it has changed lname . Holding an object-level lock does not prevent a thread from being swapped out. And if it is swapped out, it continues to hold the object-level lock. Because of this, care must be taken to ensure that all reads are blocked when the data is in an inconsistent state. CleanRead (see Listing 7.16) simply adds the synchronized method modifier to getNames() to control concurrent reading and writing.
Listing 7.16  CleanRead.javaUsing synchronized to Control Concurrent Access While Changes Are Being Made
1: public class CleanRead extends Object {
2:     private String fname;
3:     private String lname;
4:
5:     public synchronized String getNames() {
6:         return lname + , + fname;
7:     }
8:
9:     public synchronized void setNames(
10:                 String firstName,
11:                 String lastName
12:            ) {
13:
14:         print(entering setNames());
15:         fname = firstName;
16:
17:         try { Thread.sleep(1000); }
18:         catch (InterruptedException x) { }
19:
20:         lname = lastName;
21:         print(leaving setNames() - + lname + , + fname);
22:     }
23:
24:     public static void print(String msg) {
25:         String threadName = Thread.currentThread().getName();
26:         System.out.println(threadName + : + msg);
27:     }
28:
29:     public static void main(String[] args) {
30:         final CleanRead cr = new CleanRead();
31:         cr.setNames(George, Washington) ; // initially
32:
33:         Runnable runA = new Runnable() {
34:                 public void run() {
35:                     cr.setNames(Abe, Lincoln);
36:                 }
37:             };
38:
39:         Thread threadA = new Thread(runA, threadA);
40:         threadA.start();
41:
42:         try { Thread.sleep(200); }
43:         catch (InterruptedException x) { }
44:
45:         Runnable runB = new Runnable() {
46:                 public void run() {
47:                     print(getNames()= + cr.getNames());
48:                 }
49:             };
50:
51:         Thread threadB = new Thread(runB, threadB);
52:         threadB.start();
53:     }
54: }
Inside main() (lines 2953), the same actions are taken as were for DirtyRead . First, just after the CleanRead instance is created (line 30), setNames() is invoked by the main thread to initially set the names to George and Washington (line 31). After that, threadA invokes setNames() , passing in Abe and Lincoln (line 35). threadA runs for about 200 milliseconds before threadB is started (line 42).
The setNames() method is still slow, so while the names are in the process of being changed by threadA , threadB invokes the getNames() method (line 47). Because getNames() is now synchronized , threadB blocks trying to get exclusive access to the object-level lock. When threadA returns from setNames() , it automatically releases the object-level lock. Then threadB proceeds to acquire the object-level lock and enters the getNames() method. Listing 7.17 shows the output when CleanRead is run. Your output should match.
Listing 7.17  Output from CleanRead (Your Output Should Match)
1: main: entering setNames()
2: main: leaving setNames() - Washington, George
3: threadA: entering setNames()
4: threadA: leaving setNames() - Lincoln, Abe
5: threadB: getNames()=Lincoln, Abe
Although threadB invokes getNames() before threadA is finished with setNames() , it blocks waiting to get exclusive access to the object-level lock. This time, getNames() returns a valid name-pair (line 5).
  Tip If two or more threads might be simultaneously interacting with the member variables of an object, and at least one of those threads might change the values, it is generally a good idea to use synchronized to control concurrent access. If only one thread will be accessing an object, using synchronized is unnecessary and slows execution.

Toc


Java Thread Programming
Java Thread Programming
ISBN: 0672315858
EAN: 2147483647
Year: 2005
Pages: 149
Authors: Paul Hyde

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