一个竞争的例子:下面的程序是模拟一个有若干账户的银行,每个账户随机地向其他账户转钱。
bank.java有若干账户,账户之间可以相互汇钱。
public class Bank {
private final double[] accounts;
/*
* Construct the bank
* @param n the number of the accounts
* @param initialBalance the initial balance of each account
*/
public Bank(int n,double initialBalance){
accounts=new double[n];
for(int i=0;i<n;i++){
accounts[i]=initialBalance;
}
}
/*
* transfer money from one to another
* @param from the account to transfer from
* @param to the account to transfer to
* @param amount the amount to transfer
*/
public void transfer(int from,int to,double amount){
if(accounts[from]<amount)
return;
System.out.print(Thread.currentThread());
accounts[from]-=amount;
accounts[to]+=amount;
System.out.println(" "+from+"-->"+to+" total:"+getTotaleBalance());
}
/*
* get total money of the bank
* @return the total balance
*/
public double getTotaleBalance(){
double sum=0;
for(double a:accounts)
sum+=a;
return sum;
}
/*
* get the number of the accounts in the bank
* @return the number of the accounts
*/
public int size(){
return accounts.length;
}
}
TransferRunnable.java为Bank b中的每个accounts[from]创建了一个线程
public class TransferRunnable implements Runnable{
private static final int DELAY = 10;
private Bank bank;
private int fromAccount;
private double maxAmount;
/*
* construct a transfer runnable
* @param b the bank to transfer
* @param from the account the transfer form
* @param max the maximum amount money to transfer
*/
public TransferRunnable(Bank b,int from,double max){
bank=b;
fromAccount=from;
maxAmount=max;
}
public void run() {
try{
while(true){
int to=(int)(bank.size()*Math.random());
double amount =maxAmount*Math.random();
bank.transfer(fromAccount, to, amount);
Thread.sleep( (long) ((int)DELAY*Math.random()));
}
}catch(InterruptedException e){
}
}
}
测试UnsysnchBankTest.java
public class UnsysnchBankTest {
private static final int NOACCOUNTS = 100;
private static final double INITIAL_BALANCE = 1000;
public static void main(String[] args){
Bank b=new Bank(NOACCOUNTS,INITIAL_BALANCE);
for(int i=0;i<NOACCOUNTS;i++){
TransferRunnable r=new TransferRunnable(b,i,INITIAL_BALANCE);
Thread t=new Thread(r);
t.start();
}
}
}
实验一段时间后发现:
Thread[Thread-54,5,main] 54-->87 total:98917.75963258508
Thread[Thread-18,5,main] 18-->23 total:98917.75963258508
Thread[Thread-28,5,main] 28-->65 total:98917.75963258505
Thread[Thread-98,5,main] 98-->39 total:98917.75963258505
交易一段时间以后,银行总金额不足100000。
问题出现在当两个线程同时更行一个账户时,可能一个线程把另外一个线程的数据给察去了。例如,两个线程同时执行
accounts[to]+=amount;
可能执行顺序为:
线程1:读取accounts[to],假设为5000;增加amount后,accounts[to]应该为5500,但是5500还来不及返回到accounts[to]中,此时,线程2获得执行权力,读取accounts[to]为5000,增加并返回,假设此时accounts[to]为5900,然后再执行线程1的写回步骤,将accounts[to]改为5500.这个过程中accounts[to]少了900。
有两种机制可以防止代码块并发访问的干扰:
1、锁对象和条件对象
锁对象用ReentrantLock保护代码块的基本结构如下:
myLock.lock();//a ReentrantLock object
try{
critical section
}finally{
myLock.unlock();
}
这个结构确保任何时刻只有一个线程进入临界区,一旦一个线程封锁了锁对象,其他线程调用lock时被阻塞。
用一个锁保护Bank中的transfer方法如下:
Class Bank{
private ReentrantLock bankLock;
public void transfer(int from,int to,double amount) throws InterruptedException{
bankLock.lock();
try{
System.out.print(Thread.currentThread());
accounts[from]-=amount;
accounts[to]+=amount;
System.out.println(" "+from+"-->"+to+" total:"+getTotaleBalance());
}finally{
bankLock.unlock();
}
}
}
每一个bank对象都有自己的ReentrantLock对象。如果两个线程试图访问同一个Bank对象,那么锁以串行方式提供服务。但是,如果两个线程访问不同的Bank对象,每个线程得到不同的锁,他们不会发生阻塞。
锁是可重入的,线程可以重复获取自己持有的锁,所保持一个持有基数来跟踪对lock方法的嵌套调用。Transfer方法调用getTotalBalance,这也会封锁bankLock对象,此时bankLock持有计数为2,当getTotalBalance返回时,持有计数变回1,当transfer方法退出时持有计数为0。
条件对象
线程进入临界区,却发现某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得但不能做有用工作的线程。条件对象常常被称为条件变量。如下代码:
if(accounts[from]>amount)
bank.transfer(from,to,amount);
当前线程完全可能执行if语句,发现可以执行第二句,但是线程被中断,其他线程可能使accounts[from] 中的数据变得小于amount,但是原来的线程再次运行后执行第二句,但此时条件实际上已经不满足了。
一个锁对象可以有一个或多个相关的条件对象,可以用bankLock.newCondition()来获得一个条件对象。
private ReentrantLock bankLock;
private Condition sufficientFunds;
public void transfer(int from,int to,double amount) throws InterruptedException{
bankLock.lock();
try{
while(accounts[from]<amount)
sufficientFunds.await();
… …
sufficientFunds.signalAll();
}finally{
bankLock.unlock();
}
}
当发现余额不足时,调用await()方法,阻塞当前线程,并放弃锁。一段满足条件,并且接收到同一条件上的singleAll方法后,它变得可运行,当有锁可用时他继续执行。
改进后的Bank.java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
private final double[] accounts;
private ReentrantLock bankLock;
private Condition sufficientFunds;
/*
* Construct the bank
* @param n the number of the accounts
* @param initialBalance the initial balance of each account
*/
public Bank(int n,double initialBalance){
accounts=new double[n];
for(int i=0;i<n;i++){
accounts[i]=initialBalance;
}
bankLock=new ReentrantLock();
sufficientFunds=bankLock.newCondition();
}
/*
* transfer money from one to another
* @param from the account to transfer from
* @param to the account to transfer to
* @param amount the amount to transfer
*/
public void transfer(int from,int to,double amount) throws InterruptedException{
bankLock.lock();
try{
while(accounts[from]<amount)
sufficientFunds.await();
System.out.print(Thread.currentThread());
accounts[from]-=amount;
accounts[to]+=amount;
System.out.println(" "+from+"-->"+to+" total:"+getTotaleBalance());
sufficientFunds.signalAll();
}finally{
bankLock.unlock();
}
}
/*
* get total money of the bank
* @return the total balance
*/
public double getTotaleBalance(){
bankLock.lock();
try{
double sum=0;
for(double a:accounts)
sum+=a;
return sum;
}finally{
bankLock.unlock();
}
}
/*
* get the number of the accounts in the bank
* @return the number of the accounts
*/
public int size(){
return accounts.length;
}
}
2、synchronized关键字
synchronize关键字的原理是每一个java对象都有一个内部锁。也就是说
public synchronized void method(){... ...}
相当于:
public void method(){
this.intrinsicLock.lock();
try{
... ...
}finally{
his.intrinsicLock.unlock();
}
}
内部锁只有一个相关条件,wait/notifyAll相当于intrinsicCondition.await()/intrinsicCondition. signalyAll ()。用synchronized关键字的Bank.java代码如下:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
private final double[] accounts;
/*
* Construct the bank
* @param n the number of the accounts
* @param initialBalance the initial balance of each account
*/
public Bank(int n,double initialBalance){
accounts=new double[n];
for(int i=0;i<n;i++){
accounts[i]=initialBalance;
}
}
/*
* transfer money from one to another
* @param from the account to transfer from
* @param to the account to transfer to
* @param amount the amount to transfer
*/
public synchronized void transfer(int from,int to,double amount) throws InterruptedException{
while(accounts[from]<amount)
wait();
System.out.print(Thread.currentThread());
accounts[from]-=amount;
accounts[to]+=amount;
System.out.println(" "+from+"-->"+to+" total:"+getTotaleBalance());
notifyAll();
}
/*
* get total money of the bank
* @return the total balance
*/
public synchronized double getTotaleBalance(){
double sum=0;
for(double a:accounts)
sum+=a;
return sum;
}
/*
* get the number of the accounts in the bank
* @return the number of the accounts
*/
public int size(){
return accounts.length;
}
}