java多线程中,需要防止代码块受并发访问产生的干扰。比如下图的并发访问,如果不使用锁机制,就会产生问题
可以看到这里之前线程2之前的5900被后来线程1写入的5500直接覆盖了,导致add 900 这个操作消失了。
public class Bank {
private final double[] accouts;
public Bank(int n,double initialBalance) {
accouts = new double[n];
Arrays.fill(accouts, initialBalance);
}
public void transfer(int from, int to, double amount) throws InterruptedException{
if (accouts[from] < amount)
return;
System.out.println(Thread.currentThread());
accouts[from] -= amount;
System.out.printf("%10.2f from %d to %d", amount, from, to);
accouts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
}
public double getTotalBalance() {
double sum = 0;
for (double a : accouts)
sum += a;
return sum;
}
public int size(){
return accouts.length;
}
}
public class UnsynchBankTest {
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static final double MAX_AMOUNT = 1000;
public static final int DELAY = 10;
public static void main(String[] args) {
Bank bank = new Bank(NACCOUNTS,INITIAL_BALANCE);
for (int i = 0; i < NACCOUNTS; i++) {
int fromAccount = i;
Runnable r = ()->{
try{
while(true){
int toAccount = (int) (bank.size() * Math.random());
double amount = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
}catch (InterruptedException e){}
};
Thread t = new Thread(r);
t.start();
}
}
}
该程序由于没有加锁
所以会出现金额总数出错的情况,参考上图覆盖写入的情况。
所以我们要使用锁机制在使用临界资源时对其加锁(禁止其他线程并发访问或防止并发访问时产生干扰)
实现加锁就主要有下面几种方法:
一、使用ReentrantLock类+Condition条件对象
首先使用ReentraLock类就能实现简单的加锁了,在这种锁机制下,临界区需要使用try-finally括起来,因为加锁之后,若临界区里运行出现问题而抛出异常,也要确保锁被释放,否则其他线程会一直拿不到锁而无法运行。
单使用ReentrantLock的代码如下:
public class Bank {
private final double[] accouts;
private Lock banklock;
public Bank(int n,double initialBalance) {
accouts = new double[n];
Arrays.fill(accouts, initialBalance);
banklock = new ReentrantLock();
}
public void transfer(int from, int to, double amount) throws InterruptedException{
banklock.lock();
try{
System.out.println(Thread.currentThread());
accouts[from] -= amount;
System.out.printf("%10.2f from %d to %d", amount, from, to);
accouts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
}finally {
banklock.unlock();
}
}
这样加了锁之后,确实金钱总额不会再发生改变,一直是100000,但当我把账户当前剩余资金打印出来时,发现:
其实账户剩余资金为0了,竟然也还在转账!
这样显然是不可以的。那么就需要在线程进入临界区后,判断某一条件,满足之后才继续执行,否则进入阻塞状态,并释放自己持有的锁。
但这个判断又不能使用简单的if:
这样的线程完全有可能在成功通过if之后,但在transfer之前被中断,然后在线程再次运行前可能账户余额已经低于提款金额。所以就要用到条件对象了,首先创建新的Condition对象:
private Condition sufficientFunds;
如果transfer发现余额不足,就调用sufficientFunds.await();
此时当前进程就会被阻塞,并放弃锁。此时我们希望另一个线程拿到锁可以进行增加自己余额的操作。
进入阻塞状态的线程,即使锁可以,他也不能马上解除阻塞,直到另一个线程调用同一个条件上的signalAll方法为止。
所以另一个线程转账完毕后,应该调用sufficientFunds.signalAll();这一调用会重新激活因为这一条件而被阻塞的所有线程,这些线程会从等待集中被移出,再次成为可运行的,这时他应该再去检测该条件——因为signalAll方法仅仅是通知正在等待的线程:此时有可能已经满足条件,值得再次去检测该条件。
所以检测条件的语句应该这么写:
while(!(ok to proceed))
condition.await();
对bank改造如下:
public class Bank {
private final double[] accouts;
private Lock banklock;
private Condition sufficientFunds;
public Bank(int n,double initialBalance) {
accouts = new double[n];
Arrays.fill(accouts, initialBalance);
banklock = new ReentrantLock();
sufficientFunds = banklock.newCondition();
}
public void transfer(int from, int to, double amount) throws InterruptedException{
banklock.lock();
try{
while (accouts[from] < amount)
sufficientFunds.await();
System.out.println(Thread.currentThread());
accouts[from] -= amount;
System.out.printf("%10.2f from %d to %d, remain %10.2f", amount, from, to,accouts[from]);
accouts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
sufficientFunds.signalAll();
}finally {
banklock.unlock();
}
}
public double getTotalBalance() {
banklock.lock();
try {
double sum = 0;
for (double a : accouts)
sum += a;
return sum;
}finally {
banklock.unlock();
}
}
加上了锁和条件对象,运行后得到如图结果:
可以看到余额不会再出现负数的情况。