Applying this to the bank example

The second reason is a slight variation on our first rule of thumb: once you have such a strategy in place, it becomes easier to modify it. Starting with threadsafe code and gradually modifying it to improve performance is a lot easier to do than trying, in one conceptual leap, to design high- performance, threadsafe code.

12.2.2.1 Applying this to the bank example

A first attempt at ensuring data integrity in the bank example is simply to synchronize all the public methods. If only one thread can access an object at any given time, then there will not be any problems with data integrity when deposits or withdrawals are made. There may be larger data-integrity problems arising from sequences of calls, but the actual call to makeDeposit or makeWithdrawal will be fine: public synchronized Money getBalance throws RemoteException { return _balance; } public synchronized void makeDepositMoney amount throws RemoteException, NegativeAmountException { checkForNegativeAmountamount; _balance.addamount; return; } public synchronized void makeWithdrawalMoney amount throws RemoteException, OverdraftException, NegativeAmountException { checkForNegativeAmountamount; checkForOverdraftamount; _balance.subtractamount; return; } There is, however, a potential problem. Namely, many people check their balance before deciding how much money to withdraw. They perform the following sequence of actions: 1. Check balance. This is a call to getBalance . As such, it locks the instance of Account_Impl and is guaranteed to return the correct answer. 2. Get money. This is a call to makeWithdrawal . As such, it locks the instance of Account_Impl before processing the withdrawal. The problem is that the client doesnt keep the lock between the two method invocations. Its perfectly possible for a client to check the balance, find that the account has 300 in it, and then fail to withdraw 300 because, in the time between the first and second steps, another client withdrew the money. This can be frustrating for end users. The solution is to have the client maintain a lock between the steps. There are two basic ways to do this. The first is to simply add in extra remote methods so the client can explicitly manage the synchronization on Account_Impl . For example, we could add a pair of methods, getLock and releaseLock , to the interface. We also need another exception type, LockedAccountException , so the server can tell the client when it has attempted to make an operation on an account that another client has locked. This is implemented in the following code snippets: public interface Account2 extends Remote { public void getLock throws RemoteException, LockedAccountException; public void releaseLock throws RemoteException; public Money getBalance throws RemoteException, LockedAccountException; public void makeDepositMoney amount throws RemoteException, NegativeAmountException, LockedAccountException; public void makeWithdrawalMoney amount throws RemoteException, OverdraftException, LockedAccountException, NegativeAmountException; } public class Account2_Impl extends UnicastRemoteObject implements Account2 { private Money _balance; private String _currentClient; public Account_Impl2Money startingBalance throws RemoteException { _balance = startingBalance; } public synchronized void getLock throws RemoteException, LockedAccountException{ if false==becomeOwner { throw new LockedAccountException ; } return; } public synchronized void releaseLock throws RemoteException { String clientHost = wrapperAroundGetClientHost ; if null=_currentClient _currentClient.equalsclientHost { _currentClient = null; } } public syncrhonized Money getBalance throws RemoteException, LockedAccountException { checkAccess ; return _balance; } public synchronized void makeDepositMoney amount throws RemoteException, LockedAccountException, NegativeAmountException { ..... } public syncrhonized void makeWithdrawalMoney amount throws RemoteException, OverdraftException, LockedAccountException, NegativeAmountException { ... } private boolean becomeOwner { String clientHost = wrapperAroundGetClientHost ; if null=_currentClient { if _currentClient.equalsclientHost { return true; } } else { _currentClient = clientHost; return true; } return false; } private void checkAccess throws LockedAccountException { String clientHost = wrapperAroundGetClientHost ; if null=_currentClient _currentClient.equalsclientHost { return; } throw new LockedAccountException ; } private String wrapperAroundGetClientHost { String clientHost = null; try { clientHost = getClientHost ; } catch ServerNotActiveException ignored {} return clientHost } ....other private methods } This is intended to work as follows: 1. The client program begins a session by calling getLock . If the lock is in use, a LockedAccountException is thrown, and the client knows that it does not have permission to call any of the banking methods. An alternative implementation might be to make getLock a blocking operation. In this scenario, clients wait inside getLock until the account becomes available, as in the following code example: public synchronized void getLock throws RemoteException { while false==becomeOwner { try { wait ; } catch Exception ignored {} } return; } public synchronized void releaseLock throws RemoteException { String clientHost = wrapperAroundGetClientHost ; if null=_currentClient _currentClient.equalsclientHost { _currentClient = null; notifyAll ; } } 2. Once it has the lock, the client program can perform banking operations such as getBalance and makeWithdrawal . 3. After the client program is done, it must call releaseLock to make the server available for other programs. This design has quite a few problems. Among the most significant: An increase in the number of method calls Recall that, in Chapt er 7 , one of our interface design questions was, Is each conceptual operation a single method call? This design, in which getting an account balance actually entails three method calls, is in direct violation of that principle. Vulnerability to partial failure Suppose that something happens between when the client gets the lock and when the client releases the lock. For example, the network might go down, or the clients computer might crash. In this case, the lock is never released, and the account is no longer accessible from any location. [ 2] What makes this even more galling is that the integrity of the entire system depends on the client behaving properly. A program running on an unknown machine somewhere out there on a WAN simply should not have the ability to cause server failures. [ 2] Well, until someone figures out whats wrong and restarts the client application, on the same computer, to release the lock. This design may have other major faults, depending on how the application is deployed. For example, it assumes that there is at most one client program running on any given host. This may or may not be reasonable in any given deployment scenario. But its an assumption that should be verified. On the other hand, this version of Account does solve the original problem: a correctly written and noncrashing client program running on a reliable network does get to keep a lock on the account. During a single session, the client program is guaranteed that no other client program can change or even access the account data in any manner at all. Our goal is to achieve this with neither the extra method calls nor the increased vulnerability to partial failure. One solution is to automatically grant a lock and use a background thread to expire the lock when the client hasnt been active for a while. An implementation of this looks like the following: public interface Account3 extends Remote { public Money getBalance throws RemoteException, LockedAccountException; public void makeDepositMoney amount throws RemoteException, NegativeAmountException, LockedAccountException; public void makeWithdrawalMoney amount throws RemoteException, OverdraftException, LockedAccountException, NegativeAmountException; } public class Account3_Impl extends UnicastRemoteObject implements Account3 { private static final int TIMER_DURATION = 120000; Two minutes private static final int THREAD_SLEEP_TIME = 10000; 10 seconds private Money _balance; private String _currentClient; private int _timeLeftUntilLockIsReleased; public Account3_ImplMoney startingBalance throws RemoteException { _balance = startingBalance; _timeLeftUntilLockIsReleased = 0; new Threadnew CountDownTimer.start ; } public synchronized Money getBalance throws RemoteException, LockedAccountException { checkAccess ; return _balance; } public synchronized void makeDepositMoney amount throws RemoteException, LockedAccountException, NegativeAmountException { checkAccess ; ... } public synchronized void makeWithdrawalMoney amount throws RemoteException, OverdraftException, LockedAccountException, NegativeAmountException { checkAccess ; ... } private void checkAccess throws L ockedAccountException { String clientHost = wrapperAroundGetClientHost ; if null==_currentClient { _currentClient = clientHost; } else { if _currentClient.equalsclientHost { throw new LockedAccountException ; } } resetCounter ; return; } private void resetCounter { _timeUntilLockIsReleased = TIMER_DURATION; } private void releaseLock { if null=_currentClient { _currentClient = null; } } ... private class CountDownTimer implements R unnable { public void run { while true { try { Thread.sleepTHREAD_SLEEP_TIME; } catch Exception ignored {} synchronizedAccount3_Impl.this { if _timeUntilLockIsReleased 0 { _timeUntilLockIsReleased - = THREAD_SLEEP_TIME; } else { releaseLock ; } } } } } } This works a lot like the previous example. However, there are two major differences. The first is that when a method call is made, the server automatically attempts to acquire the lock on behalf of the client. The client doesnt do anything except catch an exception if the account is locked. The second difference is that the server uses a background thread to constantly check whether the client has sent any messages recently. The background threads sole mission in life is to expire the lock on the server. In order to do this, the background thread executes the following infinite loop: 1. Sleep 10 seconds. 2. See if the lock needs to be expired. If it does, expire the lock. Otherwise, decrement _timeLeftUntilLockIsReleased by 10 seconds. 3. Return to step 1. Meanwhile, every time a banking operation is invoked, _timeLeftUntilLockIs - Released is reset to two minutes. As long as the client program is executing at least one banking operation every two minutes, the lock will automatically be maintained. However, if the client finishes, if the network crashes, or if the client computer crashes, the lock will expire automatically within two minutes, and the server will once again be available. This is convenient; it solves our original problem by allowing the client to lock an account across multiple remote method invocations. In addition, it does so without any extra client overhead™it simply automatically grants short-term locks to clients whenever it is possible to do so. Its worth stopping to make sure you fully understand how this works. Using background threads to perform maintenance tasks is an important technique in distributed programming. And the idea of granting short-term privileges to clients, which must be occasionally renewed, is at the heart of RMIs distributed garbage collector. There are, however, two significant downsides to this new approach: The code is more complicated Using a background thread to expire a remote lock is not an entirely intuitive idea. Moreover, any new method, such as the transferMoney method we discussed in previous chapters, will have to somehow accommodate itself to the background thread. At the very least, it will need to call checkAccess before attempting to do anything. Threads are expensive They consume both memory and system resources. Moreover, most operating systems limit the number of threads available to a process. When most JVMs have only a limited number of threads available, using an extra thread per account server can be an unacceptable design decision. Of course, we can solve the second problem by making the first a little worse. For example, a single background thread can check the locks on all instances of Account3_Impl . That is, instead of creating a thread inside each instance of Account3_Impl , we can simply register the instance with a pre-existing thread that expires locks for all instances of Account3_Impl . Heres the constructor for this new implementation of the Account3 interface, Account3_Impl2 : public Account3_Impl2Money startingBalance throws RemoteException { _balance = startingBalance; _timeLeftUntilLockIsReleased = 0; Account3_Impl2_LockThread.getSingleton .addAccountthis; register with the lock-expiration thread } The background thread simply loops through all the registered instances of Account3_Impl2 , telling them to decrement their lock timers. To do this, we need a new method in Account3_Impl2 , decrementLockTimer : protected synchronized void decrementLockTimerint amountToDecrement { _timeLeftUntilLockIsReleased -= amountToDecrement; if _timeLeftUntilLockIsReleased 0 { _currentClient = null; } } And, finally, we need to implement the background timer: public class Account3_Impl2_LockThread extends Thread { private static final int THREAD_SLEEP_TIME = 10000; 10 seconds private static Account3_Impl2_LockThread _singleton; public static synchronized Account3_Impl2_LockThread getSingleton { if null==_singleton { _singleton = new Account3_Impl2_LockThread ; _singleton.start ; } return _singleton; } private ArrayList _accounts; private Account3_Impl2_LockThread { _accounts = new ArrayList ; } public synchronized void addAccountAccount3 newAccount { _accounts.addnewAccount; } public void run { while true { try { Thread.sleepTHREAD_SLEEP_TIME; } catch Exception ignored {} decrementLockTimers ; } } private synchronized void decrementLockTimers { Iterator i = _accounts.iterator ; while i.hasNext { Account3_Impl2 nextAccount = Account3_Impl2 i.next ; nextAccount.decrementLockTimerTHREAD_SLEEP_TIME; } } }

12.2.3 Minimize Time Spent in Synchronized Blocks