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