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();
        }
    }

加上了锁和条件对象,运行后得到如图结果:

可以看到余额不会再出现负数的情况。

posted on 2019-07-30 17:55  尤达  阅读(743)  评论(0编辑  收藏  举报

导航