27.3. Bean Properties

 
[Page 787 ( continued )]

24.9. Thread Synchronization

A shared resource may be corrupted if it is accessed simultaneously by multiple threads. The following example demonstrates the problem.

Suppose that you create and launch one hundred threads, each of which adds a penny to an account. Create a class named Account to model the account, a class named AddAPennyTask to add a penny to the account, and a main class that creates and launches threads. The relationships of these classes are shown in Figure 24.10. The program is given in Listing 24.7.

Figure 24.10. AccountWithoutSync contains an instance of Account and one hundred threads of AddAPennyTask .

Listing 24.7. AccountWithoutSync.java
(This item is displayed on pages 787 - 788 in the print version)
 1   import   java.util.concurrent.*; 2 3   public class   AccountWithoutSync { 4    private static   Account account =   new   Account();  5 6   public static void   main(String[] args) { 7  ExecutorService executor = Executors.newCachedThreadPool();  8 9  // Create and launch 100 threads  10   for   (   int   i =     ; i <   100   ; i++) { 11  executor.execute(   new   AddAPennyTask());  12 } 13 14  executor.shutdown();  15 16  // Wait until all tasks are finished  17   while   (  !executor.isTerminated()  ) { 18 } 19 20 System.out.println(   "What is balance ? "   + account.getBalance()); 21 } 22 

[Page 788]
 23  // A thread for adding a penny to the account  24   private static class   AddAPennyTask   implements   Runnable { 25   public void   run() { 26 account.deposit(   1   ); 27 } 28 } 29 30  // An inner class for account  31   private static class   Account { 32   private int   balance =     ; 33 34   public int   getBalance() { 35   return   balance; 36 } 37 38   public void   deposit(   int   amount) { 39   int   newBalance = balance + amount; 40 41  // This delay is deliberately added to magnify the  42  // data-corruption problem and make it easy to see.  43   try   { 44  Thread.sleep(   5   );  45 } 46   catch   (InterruptedException ex) { 47 } 48 49 balance = newBalance; 50 } 51 } 52 } 

The classes AddAPennyTask and Account in lines 23 “51 are inner classes. Line 4 creates an Account with initial balance . Line 11 creates a task to add a penny to the account and submit the task to the executor. Line 11 is repeated one hundred times in lines 10 “12. The program repeatedly checks whether all tasks are completed in lines 17 “18. The account balance is displayed in line 20 after all tasks are completed.

The program creates one hundred threads executed in a thread pool executor (lines 10 “12). The isTerminated() method (line 17) is used to test whether the thread is terminated .

The balance of the account is initially (line 32). When all the threads are finished, the balance should be 100 , but the output is unpredictable. As can be seen in Figure 24.11, the answers are wrong in the sample run. This demonstrates the data-corruption problem that occurs when all the threads have access to the same data source simultaneously.

Figure 24.11. The AccountWithoutSync program causes data inconsistency.


Lines 39 “49 could be replaced by one statement:

 balance = balance + amount; 

However, it is highly unlikely , although plausible, that the problem can be replicated using this single statement. The statements in lines 39 “49 are deliberately designed to magnify the data-corruption problem and make it easy to see. If you run the program several times but still do not see the problem, increase the sleep time in line 26. This will increase the chances for showing the problem of data inconsistency.


[Page 789]

What, then, caused the error in this program? Here is a possible scenario, as shown in Figure 24.12.

Figure 24.12. Task 1 and Task 2 both add 1 to the same balance.
Step balance Task 1 Task 2
1 newBalance = balance + 1 ;  
2   newBalance = balance + 1 ;
3 1 balance = newBalance;  
4 1   balance = newBalance;

In Step 1, Task 1 gets the balances from the account. In Step 2, Task 2 gets the same balances from the account. In Step 3, Task 1 writes a new balance to the account. In Step 4, Task 2 writes a new balance to the account.

The effect of this scenario is that Task 1 does nothing, because in Step 4 Task 2 overrides Task 1's result. Obviously, the problem is that Task 1 and Task 2 are accessing a common resource in a way that causes conflict. This is a common problem, known as a race condition , in multithreaded programs. A class is said to be thread-safe if an object of the class does not cause a race condition in the presence of multiple threads. As demonstrated in the preceding example, the Account class is not thread-safe.

24.9.1. The synchronized Keyword

To avoid race conditions, it is necessary to prevent more than one thread from simultaneously entering a certain part of the program, known as the critical region . The critical region in Listing 24.7 is the entire deposit method. You can use the keyword synchronized to synchronize the method so that only one thread can access the method at a time. There are several ways to correct the problem in Listing 24.7. One approach is to make Account thread-safe by adding the keyword synchronized in the deposit method in line 38, as follows :

   public      synchronized      void   deposit(   double   amount) 

A synchronized method acquires a lock before it executes. In the case of an instance method, the lock is on the object for which the method was invoked. In the case of a static method, the lock is on the class. If one thread invokes a synchronized instance method (respectively, static method) on an object, the lock of that object (respectively, class) is acquired first, then the method is executed, and finally the lock is released. Another thread invoking the same method of that object (respectively, class) is blocked until the lock is released.

With the deposit method synchronized, the preceding scenario cannot happen. If Task 1 starts to enter the method, and Task 2 is already in the method, Task 2 is blocked until Task 1 finishes the method, as shown in Figure 24.13.

Figure 24.13. Task 1 and Task 2 are synchronized.
(This item is displayed on page 790 in the print version)


Suppose you are not allowed to modify Account . You could add a new, synchronized method that invokes deposit(1) and invoke this new method from the run() method.

24.9.2. Synchronizing Statements

Invoking a synchronized instance method of an object acquires a lock on the object, and invoking a synchronized static method of a class acquires a lock on the class. A synchronized statement can be used to acquire a lock on any object, not just this object, when executing a block of the code in a method. This block is referred to as a synchronized block . The general form of a synchronized statement is as follows:


[Page 790]
   synchronized   (expr) { statements; } 

The expression expr must evaluate to an object reference. If the object is already locked by another thread, the thread is blocked until the lock is released. When a lock is obtained on the object, the statements in the synchronized block are executed, and then the lock is released.

Synchronized statements enable you to synchronize part of the code in a method instead of the entire method. This increases concurrency. Synchronized statements enable you to acquire a lock on any object so that you can synchronize the access to an object instead of to a method. You can make Listing 24.7 thread-safe by placing the statement in line 26 inside a synchronized block:

   synchronized   (account) { account.deposit(   1   ); } 

Note

Any synchronized instance method can be converted into a synchronized statement. For example, the following synchronized instance method in (a) is equivalent to (b):


 


Introduction to Java Programming-Comprehensive Version
Introduction to Java Programming-Comprehensive Version (6th Edition)
ISBN: B000ONFLUM
EAN: N/A
Year: 2004
Pages: 503

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