java多线程与线程池(二):几种实现加锁的方法(1)——ReentrantLock类+Condition条件对象
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(); } }
加上了锁和条件对象,运行后得到如图结果:
可以看到余额不会再出现负数的情况。