Chapter 17. The BooleanLock Utility

IN THIS CHAPTER

The BooleanLock class, which I present in this chapter, provides a useful encapsulation of a boolean variable that is easily and safely accessed from multiple threads. These threads can test and set the internal value and wait for it to change. The wait/notify mechanism is used internally to support waiting for the value to change, and frees external classes from the error-prone complexity of properly implementing this mechanism.

Background

Using the wait/notify mechanism effectively and correctly requires discipline. It is easy to erroneously code in such a way that a wait() is invoked just after a notify() has been issued, resulting in the notification being completely missed. This kind of mistake results in a very subtle race condition that only shows up occasionally and can be painstakingly difficult to track down. Wherever possible, the wait/notify mechanism should be encapsulated within a class to insulate external classes from the signaling complexity.

In this chapter, I present the BooleanLock class. It simply encapsulates a boolean variable and controls access to it through synchronized methods. Multiple threads can safely interact with it. The wait/notify mechanism is hidden within the class and missed notifications are impossible. It also uses the Waiting for the Full Timeout technique from Chapter 14, "Waiting for the Full Timeout."

BooleanLock can be used to conveniently signal between two threads. The first thread can block waiting until the value is set to true. When the second thread is ready, it can set the value to true and the first thread will be released from its blocked state.

BooleanLock can also be used to work around the problem of long synchronized blocks. Normally, when a thread is blocked waiting to acquire exclusive access to a lock (through synchronized), it will not respond to interrupts. In addition, there is no mechanism for specifying a timeout value that indicates how long a thread should wait to try to get into a synchronized block. The BooleanLock utility can be used to provide an interruptible, timeout-capable, technique for providing exclusive access to a block of code.

BooleanLock Class

The API for BooleanLock consists of the following methods:

public BooleanLock(boolean initialValue)
public BooleanLock()
public synchronized void setValue(boolean newValue)
public synchronized boolean waitToSetTrue(long msTimeout)
        throws InterruptedException
public synchronized boolean waitToSetFalse(long msTimeout)
        throws InterruptedException
public synchronized boolean isTrue()
public synchronized boolean isFalse()
public synchronized boolean waitUntilTrue(long msTimeout)
        throws InterruptedException
public synchronized boolean waitUntilFalse(long msTimeout)
        throws InterruptedException
public synchronized boolean waitUntilStateIs(
        boolean state, long msTimeout) throws InterruptedException

Notice that all the methods are synchronized and that many of them might throw an InterruptedException. The no-argument constructor defaults to use an initial value of false. Every time that setValue() is invoked with a new value, any and all threads blocked waiting in the waitXYZ() methods are notified of the change.

All of the waitXYZ() methods take a timeout value indicating how long they should wait for the condition to be met. A timeout of 0 indicates that the method should wait until the condition is met regardless of how long it takes. Otherwise, the timeout value is the maximum number of milliseconds that should elapse before the method returns. These methods return true if the condition was met, and false if the waiting timed out.

The full code for BooleanLock is shown in Listing 17.1.

Example 17.1. BooleanLock.java—The BooleanLock Utility

 1: public class BooleanLock extends Object {
 2:     private boolean value;
 3:
 4:     public BooleanLock(boolean initialValue) {
 5:         value = initialValue;
 6:     }
 7:
 8:     public BooleanLock() {
 9:         this(false);
10:     }
11:
12:     public synchronized void setValue(boolean newValue) {
13:         if ( newValue != value ) {
14:             value = newValue;
15:             notifyAll();
16:         }
17:     }
18:
19:     public synchronized boolean waitToSetTrue(long msTimeout)
20:             throws InterruptedException {
21:
22:         boolean success = waitUntilFalse(msTimeout);
23:         if ( success ) {
24:             setValue(true);
25:         }
26:
27:         return success;
28:     }
29:
30:     public synchronized boolean waitToSetFalse(long msTimeout)
31:             throws InterruptedException {
32:
33:         boolean success = waitUntilTrue(msTimeout);
34:         if ( success ) {
35:             setValue(false);
36:         }
37:
38:         return success;
39:     }
40: 
41:     public synchronized boolean isTrue() {
42:         return value;
43:     }
44:
45:     public synchronized boolean isFalse() {
46:         return !value;
47:     }
48:
49:     public synchronized boolean waitUntilTrue(long msTimeout)
50:             throws InterruptedException {
51:
52:         return waitUntilStateIs(true, msTimeout);
53:     }
54:
55:     public synchronized boolean waitUntilFalse(long msTimeout)
56:             throws InterruptedException {
57:
58:         return waitUntilStateIs(false, msTimeout);
59:     }
60:
61:     public synchronized boolean waitUntilStateIs(
62:                 boolean state,
63:                 long msTimeout
64:             ) throws InterruptedException {
65:
66:         if ( msTimeout == 0L ) {
67:             while ( value != state ) {
68:                 wait();  // wait indefinitely until notified
69:             }
70:
71:             // condition has finally been met
72:             return true;
73:         } 
74:
75:         // only wait for the specified amount of time
76:         long endTime = System.currentTimeMillis() + msTimeout;
77:         long msRemaining = msTimeout;
78:
79:         while ( ( value != state ) && ( msRemaining > 0L ) ) {
80:             wait(msRemaining);
81:             msRemaining = endTime - System.currentTimeMillis();
82:         }
83:
84:         // May have timed out, or may have met value,
85:         // calculate return value.
86:         return ( value == state );
87:     }
88: }

The member variable value (line 2) holds the most recent setting. The first constructor (lines 4–6) sets value to the specified initial state. The second constructor (lines 8–10) uses a default value of false for the initial state. The setValue() method (lines 12–17) is the primary mechanism used to change value. It is synchronized to coordinate access with other threads. If the value passed in truly represents a change (line 13), value is altered (line 14) and any and all waiting threads are notified of the change (line 15). The methods waitToSetTrue() and waitToSetFalse() both might end up calling setValue().

In waitToSetTrue() (lines 19–28), the calling thread waits until it has exclusive access and value is false. If several threads are waiting in this method, only one will get a chance to proceed when value is set to false. Which one of the threads gets to proceed is an arbitrary choice made by the virtual machine's thread scheduler. Internally, waitToSetTrue() invokes waitUntilFalse(), passing along the timeout information. The return value of waitUntilFalse() is stored in the local variable success (line 22). If waitUntilFalse() times out, it returns false; otherwise, it returns true. If true is returned, setValue() is used to alter value (lines 23–35). If the call to waitToSetTrue() succeeded in making a change, true is returned. If it times out while waiting, false is returned. If it is interrupted while waiting, an InterruptedException is thrown. The waitToSetFalse() method (lines 30–39) works in much the same way as waitToSetTrue().

The rest of the methods do not affect value, but simply provide information. The isTrue() (lines 41–43) and isFalse() (lines 45–47) methods provide information about the current setting of value.

The waitUntilTrue() method (lines 49–53) takes a timeout value (0 indicating never time out) and blocks until value is true, the specified number of milliseconds elapses, or an InterruptedException is triggered. If a timeout occurs, false is returned; otherwise, true is returned. The waitUntilFalse() method (lines 55–59) works in a very similar way. Both methods use the waitUntilStateIs() method as a helper.

The waitUntilStateIs() method (lines 61–87) is passed a value to match and a timeout. It blocks until the value is matched, the timeout occurs, or an InterruptedException is thrown. If it times out waiting for the condition, it returns false; otherwise, true is returned. A timeout of 0 indicates that it should block until the condition is met—regardless of how long that takes. This method uses the full-wait technique shown in Chapter 14.

Inter-Thread Signaling Using BooleanLock

BooleanLock can be a useful tool for signaling between two or more threads. For example, if one thread needs to wait for another thread to complete a task before proceeding, it can invoke the waitUntilTrue() method. When the other thread completes the task, that thread would invoke setValue(true). Using BooleanLock allows the wait/notify mechanism to remain hidden from the two threads. The class Signaling (see Listing 17.2) demonstrates this pattern.

Example 17.2. Signaling.java—Inter-thread Signaling with BooleanLock

 1: public class Signaling extends Object {
 2:     private BooleanLock readyLock;
 3:
 4:     public Signaling(BooleanLock readyLock) {
 5:         this.readyLock = readyLock;
 6:
 7:         Runnable r = new Runnable() {
 8:                 public void run() {
 9:                     try {
10:                         runWork();
11:                     } catch ( Exception x ) {
12:                         // in case ANY exception slips through
13:                         x.printStackTrace();
14:                     }
15:                 }
16:             };
17:
18:         Thread internalThread = new Thread(r, "internal");
19:         internalThread.start();
20:     }
21:
22:     private void runWork() {
23:         try {
24:             print("about to wait for readyLock to be true");
25:             readyLock.waitUntilTrue(0);  // 0 - wait forever
26:             print("readyLock is now true");
27:         } catch ( InterruptedException x ) {
28:             print("interrupted while waiting for readyLock " +
29:                     "to become true");
30:         }
31:     }
32:
33:     private static void print(String msg) {
34:         String name = Thread.currentThread().getName();
35:         System.err.println(name + ": " + msg);
36:     }
37: 
38:     public static void main(String[] args) {
39:         try {
40:             print("creating BooleanLock instance");
41:             BooleanLock ready = new BooleanLock(false);
42:        
43:             print("creating Signaling instance");
44:             new Signaling(ready);
45:
46:             print("about to sleep for 3 seconds");
47:             Thread.sleep(3000);
48:
49:             print("about to setValue to true");
50:             ready.setValue(true);
51:             print("ready.isTrue()=" + ready.isTrue());
52:         } catch ( InterruptedException x ) {
53:             x.printStackTrace();
54:         }
55:     }
56: }

The Signaling class has two parts: the instance-specific code (lines 2–31) and the static methods used for the demonstration (lines 33–55). The static method print() (lines 33–36) is used by both parts to print detailed messages.

In the main() method (lines 38–55), an instance of BooleanLock is created (line 41). Next an instance of Signaling is created with a reference to the BooleanLock passed to its constructor (line 44). The main thread then sleeps for three seconds to allow the thread within the Signaling instance to start running (line 47). Finally, the main thread signals the internal thread by invoking setValue() with true (line 50) and verifies the change in state (line 51).

The constructor for Signaling (lines 4–20) uses the self-running object pattern and also stores the reference to the BooleanLock in readyLock (line 5).

In the runWork() method (lines 22–30), the internal thread prints a message and then waits for the signal to proceed (line 25). When the BooleanLock is finally set to true, the internal thread moves on and prints another message (line 26) before returning from runWork() to die.

Listing 17.3 shows the likely output from Signaling. Your output should match closely. The only possible differences are that lines 3 and 4 might be swapped with each other and lines 6 and 7 might be swapped with each other.

Example 17.3. Likely Output from Signaling

1: main: creating BooleanLock instance
2: main: creating Signaling instance
3: main: about to sleep for 3 seconds
4: internal: about to wait for readyLock to be true
5: main: about to setValue to true
6: main: ready.isTrue()=true
7: internal: readyLock is now true

Avoiding Blocking on synchronized

The synchronized keyword can be used for methods and statement blocks to ensure that only one thread is allowed access at a time. When threads are blocked waiting to acquire the synchronized lock, they do not respond to interrupts. In addition, there is no way to limit the amount of time that a thread will wait to enter a synchronized section. This can become a problem when the work done inside a synchronized section takes a relatively long time to complete. The examples in this section demonstrate how to work around this situation.

SyncBlock

SyncBlock (see Listing 17.4) demonstrates how threads blocked on a synchronized statement ignore interrupt requests.

Example 17.4. SyncBlock.java—Threads Blocked on synchronized Ignore Interrupts

 1: public class SyncBlock extends Object {
 2:     private Object longLock;
 3:
 4:     public SyncBlock() {
 5:         longLock = new Object();
 6:     }
 7:
 8:     public void doStuff() {
 9:         print("about to try to get exclusive access " +
10:                 "to longLock");
11:
12:         synchronized ( longLock ) {
13:             print("got exclusive access to longLock");
14:             try { Thread.sleep(10000); } 
15:             catch ( InterruptedException x ) { }
16:             print("about to relinquish exclusive access to " +
17:                     "longLock");
18:         }
19:     }
20:
21:     private static void print(String msg) {
22:         String name = Thread.currentThread().getName();
23:         System.err.println(name + ": " + msg);
24:     }
25:
26:     private static Thread launch(
27:                 final SyncBlock sb,
28:                 String name
29:             ) {
30:
31:         Runnable r = new Runnable() {
32:                 public void run() {
33:                     print("in run()");
34:                     sb.doStuff();
35:                 }
36:             };
37: 
38:         Thread t = new Thread(r, name);
39:         t.start();
40:
41:         return t;
42:     }
43:
44:     public static void main(String[] args) {
45:         try {
46:             SyncBlock sb = new SyncBlock();
47:    
48:             Thread t1 = launch(sb, "T1");
49:             Thread.sleep(500);
50:
51:             Thread t2 = launch(sb, "T2");
52:             Thread t3 = launch(sb, "T3");
53:
54:             Thread.sleep(1000);
55:
56:             print("about to interrupt T2");
57:             t2.interrupt();
58:             print("just interrupted T2");
59:
60:         } catch ( InterruptedException x ) {
61:             x.printStackTrace();
62:         }
63:     }
64: }

The SyncBlock class has two parts: the instance-specific code (lines 2–19) and the static methods used for the demonstration (lines 21–63). The static method print() (lines 21–24) is used by both parts to print detailed messages.

In main() (lines 44–63), an instance of SyncBlock is constructed to be shared by three threads (line 46). The launch() method (lines 26–42) takes a reference to this shared instance, creates a new Runnable (and Thread to run it), and uses this thread to simply invoke the doStuff() method of SyncBlock (line 34). A reference to this Thread is returned by launch().

The doStuff() method (lines 8–19) prints a message (lines 9–10) and then tries to acquire an exclusive lock on longLock (line 12). Once a thread gets exclusive access, it prints a message, sleeps for 10 seconds, and prints another message (lines 13–17). While one thread is inside sleeping for 10 seconds, other threads line up and block waiting to get exclusive access.

Back in main(), thread T1 is launched (line 48). It gets right into the synchronized block of doStuff() because no other threads are competing with it. After a half-second sleep, T2 is launched, immediately followed by T3 (lines 49–52). These two threads block on the synchronized statement (line 12) waiting for T1 to finish up and release the lock.

Meanwhile, the main thread sleeps for one second to be sure that T2 and T3 get a chance to block (line 54). Next, the main thread interrupts T2 (line 57) hoping to free it from its blocked state. As Listing 17.5 shows, this interrupt does not free T2.

Example 17.5. Possible Output from SyncBlock

 1: T1: in run()
 2: T1: about to try to get exclusive access to longLock
 3: T1: got exclusive access to longLock
 4: T3: in run()
 5: T3: about to try to get exclusive access to longLock
 6: T2: in run()
 7: T2: about to try to get exclusive access to longLock
 8: main: about to interrupt T2
 9: main: just interrupted T2
10: T1: about to relinquish exclusive access to longLock
11: T3: got exclusive access to longLock
12: T3: about to relinquish exclusive access to longLock
13: T2: got exclusive access to longLock
14: T2: about to relinquish exclusive access to longLock

Lines 8 and 9 are printed just before and just after T2 is interrupted. As you can see, this does not have the desired effect; T2 continues to wait and ultimately gains access (lines 13–14).

InterruptibleSyncBlock

InterruptibleSyncBlock (see Listing 17.6) uses the functionality of BooleanLock to have threads blocked on an interruptible wait() statement instead of a non-interruptible synchronized statement.

Example 17.6. InterruptibleSyncBlock.java—An Alternative Waiting List

 1: public class InterruptibleSyncBlock extends Object {
 2:     private Object longLock;
 3:     private BooleanLock busyLock;
 4:
 5:     public InterruptibleSyncBlock() {
 6:         longLock = new Object();
 7:         busyLock = new BooleanLock(false);
 8:     }
 9:
10:     public void doStuff() throws InterruptedException {
11:         print("about to try to get exclusive access " +
12:                 "to busyLock");
13:         busyLock.waitToSetTrue(0);
14:
15:         try {
16:             print("about to try to get exclusive access " +
17:                     "to longLock");
18:             synchronized ( longLock ) {
19:                 print("got exclusive access to longLock");
20:                 try { 
21:                     Thread.sleep(10000);
22:                 } catch ( InterruptedException x ) { 
23:                     // ignore
24:                 }
25:                 print("about to relinquish exclusive access " +
26:                         "to longLock");
27:             }
28:         } finally {
29:             print("about to free up busyLock");
30:             busyLock.setValue(false);
31:         }
32:     }
33:
34:     private static void print(String msg) {
35:         String name = Thread.currentThread().getName();
36:         System.err.println(name + ": " + msg);
37:     }
38: 
39:     private static Thread launch(
40:                 final InterruptibleSyncBlock sb,
41:                 String name
42:             ) {
43:
44:         Runnable r = new Runnable() {
45:                 public void run() {
46:                     print("in run()");
47:                     try {
48:                         sb.doStuff();
49:                     } catch ( InterruptedException x ) {
50:                         print("InterruptedException thrown " +
51:                                 "from doStuff()");
52:                     }
53:                 }
54:             };
55:        
56:         Thread t = new Thread(r, name);
57:         t.start();
58:
59:         return t;
60:     }
61:
62:     public static void main(String[] args) {
63:         try {
64:             InterruptibleSyncBlock sb =
65:                     new InterruptibleSyncBlock();
66:    
67:             Thread t1 = launch(sb, "T1");
68:             Thread.sleep(500);
69:
70:             Thread t2 = launch(sb, "T2");
71:             Thread t3 = launch(sb, "T3");
72:
73:             Thread.sleep(1000);
74:
75:             print("about to interrupt T2");
76:             t2.interrupt();
77:             print("just interrupted T2");
78:
79:         } catch ( InterruptedException x ) {
80:             x.printStackTrace();
81:         }
82:     }
83: }

InterruptibleSyncBlock is very similar to SyncBlock, except for a few enhancements inside the doStuff() method (lines 10–32). A new member variable busyLock has been added (line 3), and it is an instance of BooleanLock (line 7). It is used inside doStuff() to control concurrent access. The waitToSetTrue() method is invoked by each thread as it enters doStuff() (line 13). Because a timeout of 0 is passed, threads will wait forever to get their turn—or at least until they are interrupted. When one of the threads gets notified that it can set the state to true, it does so and returns from waitToSetTrue(). All the other threads will get notified, but will see that some other thread has beat them and will go back to waiting inside waitToSetTrue(). The thread that gets to set busyLock to true then proceeds into the try block (lines 15–27). No matter how this thread leaves the try block (even by throwing an Exception or Error), it will enter the finally block (lines 28–31) and set busyLock back to false (line 30) to allow another thread to get into the exclusive section.

Because busyLock is now protecting the synchronized block, there might not be any need to synchronize on longLock. If there are other areas where longLock was being used to control concurrent access (which is not the case in the example), you have two options. One option is to protect concurrent access by using busyLock everywhere that longLock was previously used. The other option is to continue to have longLock protect all sections (as in this example) and to have busyLock provide a preliminary, interruptible barrier for long-running sections of code.

Listing 17.7 shows possible output when InterruptibleSyncBlock is run.

Example 17.7. Possible Output from InterruptibleSyncBlock

 1: T1: in run()
 2: T1: about to try to get exclusive access to busyLock
 3: T1: about to try to get exclusive access to longLock
 4: T1: got exclusive access to longLock
 5: T2: in run()
 6: T2: about to try to get exclusive access to busyLock
 7: T3: in run()
 8: T3: about to try to get exclusive access to busyLock
 9: main: about to interrupt T2
10: main: just interrupted T2
11: T2: InterruptedException thrown from doStuff()
12: T1: about to relinquish exclusive access to longLock
13: T1: about to free up busyLock
14: T3: about to try to get exclusive access to longLock
15: T3: got exclusive access to longLock
16: T3: about to relinquish exclusive access to longLock
17: T3: about to free up busyLock

This time, when T2 is interrupted (lines 9–10), it throws an InterruptedException (line 11) and is broken out of the blocked state.

This use of BooleanLock is great when a synchronized section of code might run for a long time. Also consider passing a non-zero timeout so that threads will not block forever waiting for exclusive access.

Noticing Brief Changes in Value Using TransitionDetector

The BooleanLock class is very useful when you need to know that the value inside has been changed and remains changed. As an example, consider this situation where there is an instance of BooleanLock

BooleanLock bl = new BooleanLock(false);

and you have two threads, threadA and threadB, each simultaneously running this code:

synchronized ( bl ) {
    bl.waitUntilTrue();
    bl.setValue(false);
}

Both threadA and threadB are blocked waiting inside the waitUntilTrue() method of BooleanLock. If a third thread, threadC, comes along and sets the value to true, both threadA and threadB are notified and compete to reacquire the lock before returning from wait() inside waitUntilTrue(). In this case, let's say that threadA reacquires the lock first, returns from wait(), returns from waitUntilTrue, and then invokes setValue() right away, passing in false. This all occurs before threadB returns from wait() because the synchronized statement block ensures that the object-level lock on bl is held until setValue() completes.

After threadA has set the value of bl back to false and left the synchronized block, threadB is able to reacquire the lock. threadB reacquires the lock, returns from the wait() inside waitUntilTrue(), and sees that the value is not true. Because the value is not true, threadB invokes wait() again and does not return from waitUntilTrue().

In many cases, this is the type of behavior that you need. However, there are times when you'll want your code to detect that the value changed from false to true even if the value is no longer true. BooleanLock does not provide this kind of information. You can used a class like TransitionDetector (see Listing 17.8) when you need to know that a change occurred—no matter how briefly the value remained in the new state.

Example 17.8. TransitionDetector.java—Sensing All the Changes in Value

 1: public class TransitionDetector extends Object {
 2:     private boolean value;
 3:     private Object valueLock;
 4:     private Object falseToTrueLock;
 5:     private Object trueToFalseLock;
 6:
 7:     public TransitionDetector(boolean initialValue) {
 8:         value = initialValue;
 9:         valueLock = new Object();
10:         falseToTrueLock = new Object();
11:         trueToFalseLock = new Object();
12:     }
13:
14:     public void setValue(boolean newValue) {
15:         synchronized ( valueLock ) {
16:             if ( newValue != value ) {
17:                 value = newValue;
18:                
19:                 if ( value ) {
20:                     notifyFalseToTrueWaiters();
21:                 } else {
22:                     notifyTrueToFalseWaiters();
23:                 }
24:             }
25:         }
26:     }
27:
28:     public void pulseValue() {
29:         // Sync on valueLock to be sure that no other threads
30:         // get into setValue() between these two setValue()
31:         // calls.
32:         synchronized ( valueLock ) {
33:             setValue(!value);
34:             setValue(!value);
35:         }
36:     }
37:
38:     public boolean isTrue() {
39:         synchronized ( valueLock ) {
40:             return value;
41:         }
42:     }
43: 
44:     public void waitForFalseToTrueTransition()
45:             throws InterruptedException {
46:
47:         synchronized ( falseToTrueLock ) {
48:             falseToTrueLock.wait();
49:         }
50:     }
51:
52:     private void notifyFalseToTrueWaiters() {
53:         synchronized ( falseToTrueLock ) {
54:             falseToTrueLock.notifyAll();
55:         }
56:     }
57:
58:     public void waitForTrueToFalseTransition()
59:             throws InterruptedException {
60:
61:         synchronized ( trueToFalseLock ) {
62:             trueToFalseLock.wait();
63:         }
64:     }
65:
66:     private void notifyTrueToFalseWaiters() {
67:         synchronized ( trueToFalseLock ) {
68:             trueToFalseLock.notifyAll();
69:         }
70:     }
71:
72:     public String toString() {
73:         return String.valueOf(isTrue());
74:     }
75: }

TransitionDetector maintains three locks to control access and communication about the boolean value it encapsulates. In the constructor (lines 7–12), an initial value for value is passed in and stored. The three objects used for locking and inter-thread communication are created (lines 9–11). The valueLock object is used to control simultaneous access to value. The falseToTrueLock facilitates the use of the wait/notify mechanism to notify threads waiting for value to transition from false to true. The trueToFalseLock object is used to notify threads waiting for value to change from true to false.

Threads that want to know when value changes from false to true invoke the waitForFalseToTrueTransition() method (lines 44–50). Inside, the invoking thread simply waits on the falseToTrueLock for notification (line 48). On the other hand, threads that want to know when value changes from true to false invoke the waitForTrueToFalseTransition() method (lines 58–64). The threads that invoke this method simply wait on the trueToFalseLock for notification (line 62).

Inside setValue() (lines 14–26), exclusive access to value is controlled by synchronizing on valueLock (line 15). If the new value is indeed different than the current value (line 16), value is changed (line 17). Depending on which transition occurred, one of the two methods notifyFalseToTrueWaiters() or notifyTrueToFalseWaiters() is invoked to signal any threads waiting for that particular change (lines 19–23).

The private method notifyFalseToTrueWaiters() (lines 52–56) is only called from within setValue() while the lock is still held on valueLock. Inside, any and all threads waiting on falseToTrueLock are notified (line 54). The notifyTrueToFalseWaiters() method (lines 66–70) works similarly, but instead notifies threads waiting on the trueToFalseLock (line 68).

The pulseValue() method (lines 28–36) is used to momentarily change value. If value is false, it is changed to true and then changed right back to false. Likewise, if value is true, it is changed to false and immediately back to true. Both calls to setValue() occur inside a synchronized block to ensure that no other thread can get a lock on valueLock while between the calls (lines 32–35). Calling pulseValue() has the effect of notifying both the threads waiting inside waitForTrueToFalseTransition() and the threads waiting inside waitForFalseToTrueTransition().

TransitionDetectorMain (Listing 17.9) demonstrates how TransitionDetector can be used.

Example 17.9. TransitionDetectorMain.java—Demonstrating the Use of TransitionDetector

 1: public class TransitionDetectorMain extends Object {
 2:     private static Thread startTrueWaiter(
 3:                 final TransitionDetector td,
 4:                 String name
 5:             ) {
 6:
 7:         Runnable r = new Runnable() {
 8:                 public void run() {
 9:                     try {
10:                         while ( true ) {
11:                             print("about to wait for false-to-" +
12:                                 "true transition, td=" + td);
13:
14:                             td.waitForFalseToTrueTransition();
15:
16:                             print("just noticed for false-to-" +
17:                                 "true transition, td=" + td);
18:                         }
19:                     } catch ( InterruptedException ix ) {
20:                         return;
21:                     }
22:                 }
23:             };
24:
25:         Thread t = new Thread(r, name);
26:         t.start();
27:
28:         return t;
29:     }
30:
31:     private static Thread startFalseWaiter(
32:                 final TransitionDetector td,
33:                 String name
34:             ) {
35:
36:         Runnable r = new Runnable() {
37:                 public void run() {
38:                     try {
39:                         while ( true ) {
40:                             print("about to wait for true-to-" +
41:                                 "false transition, td=" + td);
42: 
43:                             td.waitForTrueToFalseTransition();
44:
45:                             print("just noticed for true-to-" +
46:                                 "false transition, td=" + td);
47:                         }
48:                     } catch ( InterruptedException ix ) {
49:                         return;
50:                     }
51:                 }
52:             };
53:
54:         Thread t = new Thread(r, name);
55:         t.start();
56:
57:         return t;
58:     }
59:
60:     private static void print(String msg) {
61:         String name = Thread.currentThread().getName();
62:         System.err.println(name + ": " + msg);
63:     }
64:
65:     public static void main(String[] args) {
66:         try {
67:             TransitionDetector td =
68:                     new TransitionDetector(false);
69:
70:             Thread threadA = startTrueWaiter(td, "threadA");
71:             Thread threadB = startFalseWaiter(td, "threadB");
72:
73:             Thread.sleep(200);
74:             print("td=" + td + ", about to set to 'false'");
75:             td.setValue(false);
76:
77:             Thread.sleep(200);
78:             print("td=" + td + ", about to set to 'true'");
79:             td.setValue(true);
80:
81:             Thread.sleep(200);
82:             print("td=" + td + ", about to pulse value");
83:             td.pulseValue();
84:
85:             Thread.sleep(200);
86:             threadA.interrupt();
87:             threadB.interrupt();
88:         } catch ( InterruptedException x ) {
89:             x.printStackTrace();
90:         }
91:     }
92: }

Inside main() (lines 65–91), an instance of TransitionDetector is constructed (line 68) and two threads are started to wait for transitions. After sleeping for a short period, the main thread invokes setValue(), passing in false (lines 73–75). 200 milliseconds later, main again invokes setValue(), this time passing in true (lines 77–79). After another 200 milliseconds pass, the main thread calls pulseValue() to notify both kinds of transition waiters (lines 81–83). Finally, main interrupts both of the threads that were started to get them to die (lines 86–87).

The startTrueWaiter() method (lines 2–29) creates a new thread that continually waits on waitForFalseToTrueTransition() (line 14). This new thread is called threadA. Each time through the infinite while loop, threadA prints information about the state of the instance of TransitionDetector (lines 11–17).

The startFalseWaiter() method (lines 31–58) works much like startTrueWaiter() but instead waits on waitForTrueToFalseTransition() (line 43).

Listing 17.10 shows the output produced when TransitionDetectorMain is run. Due to the indeterminate nature of the thread scheduler, your output might vary slightly.

Example 17.10. Possible Output from TransitionDetectorMain

 1: threadA: about to wait for false-to-true transition, td=false
 2: threadB: about to wait for true-to-false transition, td=false
 3: main: td=false, about to set to 'false'
 4: main: td=false, about to set to 'true'
 5: threadA: just noticed for false-to-true transition, td=true
 6: threadA: about to wait for false-to-true transition, td=true
 7: main: td=true, about to pulse value
 8: threadB: just noticed for true-to-false transition, td=true
 9: threadB: about to wait for true-to-false transition, td=true
10: threadA: just noticed for false-to-true transition, td=true
11: threadA: about to wait for false-to-true transition, td=true

Both threadA and threadB start up and wait for their desired transitions (lines 1–2). Note that it does not matter what the current setting of value is—both threads are waiting for value to change. The main thread sets value to false, but this has no effect because value was already false (line 3). Next, main sets value to true (line 4), which causes threadA to return from waitForFalseToTrueTransition() (line 5) and to loop again (line 6). After another 200- millisecond delay, main invokes pulseValue() (line 7). This causes both threadA and threadB to return and report that they noticed the transitions that they were waiting for (lines 8–11). Notice that value is true before and after pulseValue() is invoked.

Summary

BooleanLock can be used to simplify inter-thread communication. It nicely encapsulates the error-prone complexity of the wait/notify mechanism. Another useful application of BooleanLock is to provide an interruptible alternative to control access to synchronized blocks of code. TransitionDetector can be used when the fact that the internal value has changed is important, regardless of whether it has been changed back to its original value.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.250.153