public class BankAccount { private double balance; public BankAccount(){ balance=0; } public void deposit(double amount) { System.out.println("Depositing "+amount); double newBalance=balance+amount; System.out.println(", new balance is "+newBalance); balance=newBalance; } } public void withdraw(double amount) { System.out.println("withdrawing"+amount); double newBalance=balance-amount; System.out.println(",new balance is"+newBalance); balance=newBalance; } } public double getBalance() { return balance; } }
public class DepositRunnable implements Runnable{ private static final int DELAY=1; private BankAccount account; private double amount; private int count; public DepositRunnable(BankAccount anAccount,double anAmount,int aCount) { account=anAccount; amount=anAmount; count=aCount; } public void run() { try { for (int i = 1; i <= count; i++) { account.deposit(amount); Thread.sleep(DELAY); } } catch (InterruptedException exception) { // TODO: handle exception } } }
public class WithdrawRunnable implements Runnable{ private static final int DELAY=10; private BankAccount account; private double amount; private int count; public WithdrawRunnable(BankAccount anAccount,double anAmount,int aCount) { account=anAccount; amount=anAmount; count=aCount; } public void run() { try { for (int i = 1; i <= count; i++) { account.withdraw(amount); Thread.sleep(DELAY); } } catch (InterruptedException exception) { // TODO: handle exception } } }
/*该程序运行对同一个银行账户进行存取款操作的 * 两个线程 */ public class BankAccountThreadTester { public static void main(String[] args) { BankAccount account=new BankAccount(); final double AMOUNT=100; final int REPETITIONS=1000; DepositRunnable d=new DepositRunnable(account,AMOUNT,REPETITIONS); WithdrawRunnable w=new WithdrawRunnable(account,AMOUNT,REPETITIONS); Thread t1=new Thread(d); Thread t2=new Thread(w); t1.start(); t2.start(); } }
after a withdrawal and deposit should again be 0, not 100. Because the deposit
method was interrupted, it used the old balance (before the withdrawal) to compute
the value of its local newBalance variable. Later, when it was activated again, it used
that newBalance value to overwrite the changed balance variable.
As you can see, each thread has its own local variables, but all threads share
access to the balance instance variable. That shared access creates a problem. This
problem is often called a race condition. All threads, in their race to complete their
respective tasks, manipulate a shared variable, and the end result depends on which
of them happens to win the race.
You might argue that the reason for this problem is that we made it too easy to
interrupt the balance computation. Suppose the code for the deposit method is reorganized
like this:
resources, like this:
public class BankAccount
{
private Lock balanceChangeLock;
. . .
public BankAccount()
{
balanceChangeLock = new ReentrantLock();
. . .
}
}
All code that manipulates the shared resource is surrounded by calls to lock and
unlock the lock object:
balanceChangeLock.lock();
Manipulate the shared resource 处理共享资源的代码
balanceChangeLock.unlock();
try
{
Manipulate the shared resource
}
finally
{
balanceChangeLock.unlock();
}
For example, here is the code for the deposit method:
public void deposit(double amount)
{
balanceChangeLock.lock();
try
{
System.out.print("Depositing " + amount);
double newBalance = balance + amount;
System.out.println(", new balance is " + newBalance);
balance = newBalance;
}
finally
{
balanceChangeLock.unlock();
}
}
If a thread calls lock while another thread owns the lock, it is temporarily deactivated.
The thread scheduler periodically reactivates such a thread so that it can again
try to acquire the lock. If the lock is still unavailable, the thread is again deactivated.
Eventually, when the lock is available because the original thread unlocked it, the
waiting thread can acquire A the lock.
account.withdraw(amount);
{
balanceChangeLock.lock();
try
{
while (balance < amount)
Wait for the balance to grow
. . .
}
finally
{
balanceChangeLock.unlock();
}
}
But how can we wait for the balance to grow? We can’t simply call sleep inside the
withdraw method. If a thread sleeps after acquiring a lock, it blocks all other threads
that want to use the same lock. In particular, no other thread can successfully execute
the deposit method. Other threads will call deposit, but they will simply be
blocked until the withdraw method exits. But the withdraw method doesn’t exit until it
has funds available. This is the deadlock situation that we mentioned earlier.
To overcome this problem, we use a condition object. Condition objects allow a
thread to temporarily release a lock, so that another thread can proceed, and to
regain the lock at a later time.
In the telephone booth analogy, suppose that the coin reservoir of the telephone
is completely filled, so that no further calls can be made until a service technician
removes the coins. You don’t want the person in the booth to go to sleep with the
door closed. Instead, think of the person leaving the booth temporarily. That gives
another person (hopefully a service technician) a chance to enter the booth.
Each condition object belongs to a specific lock object. You obtain a condition
object with the newCondition method of the Lock interface. For example,
public class BankAccount
{
private Lock balanceChangeLock;
private Condition sufficientFundsCondition;
. . .
public BankAccount()
{
balanceChangeLock = new ReentrantLock();
sufficientFundsCondition = balanceChangeLock.newCondition();
. . .
}
}
You need to implement an appropriate
test. For as long as the test is not fulfilled, call the await method on the condition
object:
public void withdraw(double amount)
{
balanceChangeLock.lock();
try
{
while (balance < amount)
sufficientFundsCondition.await();
. . .
}
finally
{
balanceChangeLock.unlock();
}
}
When a thread calls await, it is not simply deactivated in the same way as a thread
that reaches the end of its time slice. Instead, it is in a blocked state, and it will not
be activated by the thread scheduler until it is unblocked. To unblock, another
thread must execute the signalAll method on the same condition object. The
signalAll method unblocks all threads waiting on the condition. They can then
compete with all other threads that are waiting for the lock object. Eventually, one
of them will gain access to the lock, and it will exit from the await method.
In our situation, the deposit method calls signalAll:
public void deposit(double amount)
{
balanceChangeLock.lock();
try
{
. . .
sufficientFundsCondition.signalAll();
}
finally
{
balanceChangeLock.unlock();
}
}
The call to signalAll notifies the waiting threads that sufficient funds may be available,
and that it is worth testing the loop condition again。
There is also a signal method, which randomly picks just one thread that is waiting
on the object and unblocks it. The signal method can be more efficient, but it is
useful only if you know that every waiting thread can actually proceed. In general,
you don’t know that, and signal can lead to deadlocks. For that reason, we recommend
that you always call signalAll.
The await method can throw an InterruptedException. The withdraw method propagates
that exception, because it has no way of knowing what the thread that calls the
withdraw method wants to do if it is interrupted.
With the calls to await and signalAll in the withdraw and deposit methods, we can
launch any number of withdrawal and deposit threads without a deadlock. If you
run the sample program, you will note that all transactions are carried out without
ever reaching a negative balance.
package 银行账户; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class BankAccount { private double balance; private Lock balanceChangeLock; private Condition sufficientFundsCondition; public BankAccount(){ balance=0; balanceChangeLock=new ReentrantLock(); sufficientFundsCondition=balanceChangeLock.newCondition(); } public void deposit(double amount) { balanceChangeLock.lock(); try { System.out.println("Depositing "+amount); double newBalance=balance+amount; System.out.println(", new balance is "+newBalance); balance=newBalance; sufficientFundsCondition.signalAll(); } finally{ balanceChangeLock.unlock(); } } public void withdraw(double amount) throws InterruptedException { balanceChangeLock.lock(); try { while(balance<amount) { sufficientFundsCondition.await(); } System.out.println("withdrawing"+amount); double newBalance=balance-amount; System.out.println(",new balance is"+newBalance); balance=newBalance; } finally{ balanceChangeLock.unlock(); } } public double getBalance() { return balance; } }
Object Locks and Synchronized Methods
The Lock and Condition classes were added in Java version 5.0. They overcome limitations of
the thread synchronization mechanism in earlier Java versions. In this note, we discuss that
classic mechanism.
Every Java object has one built-in lock and one built-in condition variable. The lock
works in the same way as a ReentrantLock object. However, to acquire the lock, you call a
synchronized method.
You simply tag all methods that contain thread-sensitive code (such as the deposit and
withdraw methods of the BankAccount class) with the synchronized reserved word.
public class BankAccount
{
public synchronized void deposit(double amount)
{
System.out.print("Depositing " + amount);
double newBalance = balance + amount;
System.out.println(", new balance is " + newBalance);
balance = newBalance;
}
public synchronized void withdraw(double amount)
{
. . .
}
. . .
}
When a thread calls a synchronized method on a BankAccount object, it owns that object’s lock
until it returns from the method and thereby unlocks the object. When an object is locked by
one thread, no other thread can enter a synchronized method for that object. When another
thread makes a call to a synchronized method for that object, the other thread is automatically
deactivated, and it needs to wait until the first thread has unlocked the object again.
In other words, the synchronized reserved word automatically implements the lock/try/
finally/unlock idiom for the built-in lock.
The object lock has a single condition variable that you manipulate with the wait, notifyAll, and notify methods of the Object class. If you call x.wait(), the current thread is
added to the set of threads that is waiting for the condition of the object x. Most commonly,
you will call wait(), which makes the current thread wait on this. For example,
public synchronized void withdraw(double amount)
throws InterruptedException
{
while (balance < amount)
wait();
. . .
}
The call notifyAll() unblocks all threads that are waiting for this:
public synchronized void deposit(double amount)
{
. . .
notifyAll();
}
This classic mechanism is undeniably simpler than using explicit locks and condition variables.
However, there are limitations. Each object lock has one condition variable, and you
can’t test whether another thread holds the lock. If these limitations are not a problem, by all
means, go ahead and use the synchronized reserved word. If you need more control over
threads, the Lock and Condition interfaces give you additional flexibility.