24.10. (Optional) Synchronization Using Locks |
In Listing 24.7, one hundred tasks deposit a penny to the same account concurrently, which causes conflict. To avoid it, you can simply use the synchronized keyword in the deposit method, as follows :
public synchronized void deposit( double amount)
A synchronized instance method implicitly acquires a lock on the instance before it executes the method.
JDK 1.5 enables you to use locks explicitly. The new locking features are flexible and give you more control for coordinating threads. A lock is an instance of the Lock interface, which declares the methods for acquiring and releasing locks, as shown in Figure 24.14. A lock may also use the newCondition() method to create any number of Condition objects, which can be used for thread communications.
ReentrantLock is a concrete implementation of Lock for creating mutually exclusive locks. You can create a lock with the specified fairness policy . True fairness policies guarantee that the longest-wait thread will obtain the lock first. False fairness policies grant a lock to a waiting thread without any access order. Programs using fair locks accessed by many threads may have poorer overall performance than those using the default setting, but have smaller variances in times to obtain locks and guarantee lack of starvation .
Listing 24.8 revises AccountWithoutSync.java in Listing 24.7 to synchronize the account modification using explicit locks.
1 import java.util.concurrent.*; 2 import java.util.concurrent.locks.* ; 3 4 public class AccountWithSyncUsingLock { 5 private static Account account = new Account(); 6 7 public static void main(String[] args) { 8 ExecutorService executor = Executors.newCachedThreadPool(); 9 10 // Create and launch 100 threads 11 for ( int i = ; i < 100 ; i++) { 12 executor.execute( new AddAPennyTask()); 13 } 14 15 executor.shutdown(); 16 17 // Wait until all tasks are finished 18 while (!executor.isTerminated()) { 19 } 20 21 System.out.println( "What is balance ? " + account.getBalance()); 22 } 23 24 // A thread for adding a penny to the account 25 public static class AddAPennyTask implements Runnable { 26 public void run() { 27 account.deposit( 1 ); 28 } 29 } 30 31 // An inner class for account 32 public static class Account { 33 private static Lock lock = new ReentrantLock(); // Create a lock 34 private int balance = ; 35 36 public int getBalance() { 37 return balance; 38 } 39 40 public void deposit( int amount) { 41 lock.lock(); // Acquire the lock 42 43 try { 44 int newBalance = balance + amount; 45 46 // This delay is deliberately added to magnify the 47 // data-corruption problem and make it easy to see. 48 Thread.sleep( 5 ); 49 50 balance = newBalance; 51 } 52 catch (InterruptedException ex) { 53 } 54 finally { 55 lock.unlock(); // Release the lock 56 } 57 } 58 } 59 } |
Line 33 creates a lock, line 41 acquires the lock, and line 55 releases the lock.
Tip
It is a good practice to always immediately follow a call to lock() with a try-catch block and release the lock in the finally clause, as shown in lines 41 “56, to ensure that the lock is always released. |
The example in Listing 24.7 using the synchronized method is simpler than the example in Listing 24.8 using a lock. In general, using synchronized methods or statements is simpler than using explicit locks for mutual exclusion. However, using explicit locks is more intuitive and flexible to synchronize threads with conditions, as you will see in the next section.