Java(8-2)多线程的同步和条件对象

这一节,我们要一口气介绍完多线程同步和条件对象的原因,锁机制,条件对象的使用以及我们之前写的那个银行转钱系统的剩余代码 。

Part 1 同步
为什么要有同步呢? 我们从之前多线程的一个例子来看:有很多用户都分别在进行交易,假若其中一个线程,在正在进行存钱操作,但是还未结束的时候,它在CPU的时间片就已经用完了,这个线程被中断了!而他还没把存的钱写入记录!却转而去运行其他线程了,等下一次他的时间片到来时,之前关于存钱的更新在内存中已经被擦去,那么,我们银行系统的总账户余额就会变少(你愿意把钱存进这样的银行嘛? 反正我不愿意),这显然是我们不希望遇见的!

为了防止这种情况出现,我们有两种防止代码受并发访问的干扰:一种是在Java SE 5.0引入的ReetrantLock类; 一种是synchronized关键字,将自动提供一个锁及其相关条件。

Part 1.1 锁对象介绍

锁对象的应用如下:

myLock.lock()// 一个锁对象上锁
try{
    //进行操作
}finally{
    myLock.unlock();// 在这里对锁对象进行解锁
}

这一结构确保了任何时刻只有一个线程进入临界区,这样我们银行之前存入的操作就不会被擦掉了。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。其他线程调用lock时,他们将被阻塞,知道第一个线程释放所对象。

Tip:
如果两个线程试图放问同一个Bank对象(每个Bank都有一个锁),那么锁将以串行的方式提供服务;但是,如果两个线程访问不同的Bank对象,每一个线程将得到不同的所对象,两个线程都不会发生阻塞。

锁是可重入的,因为线程可以重复得获得已经持有的锁。锁保持一个持有计数来跟踪对lock方法的嵌套使用。线程每一次调用lock方法都会使计数增加,每次调用Unlock都会使计数减少,由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。

举个例子,在我们最终的代码中,transfer方法调用getTotalBalance方法,这也会封锁bankLock对象,此时bankLock对象持有的锁的计数为2。当getTotalBalance方法退出的时候,持有计数变回1。当transfer方法退出时,持有计数变为0,线程释放锁。

Part 1.2 条件对象
我们对程序上了锁,这样避免了一些令我们难过的情况,不过事实上,事情比我们往往比我们想的还要复杂,现在设想:银行的一个线程,进入了临界区,他想进行取钱操作,但是发现没有足够的钱可以取出(可能是工资还没打到用户的账户上),只有钱足够时,才能去出钱,但是他现在又对本身的Bank对象上了锁,因此别的线程无权对这个Bank对象进行操作,这时,我们就需要使用条件对象了 。

条件对象的使用如下:

    private Condition sufficientFunds;
    ...
    public Bank()
    {
        ...
        sufficientFunds = bankLock.newCondition();
    }
}

如果transfer方法发现余额不足,他会调用:

sufficientFunds.await();

当前线程现在被阻塞了,并放弃了锁。我们希望这样可以使得另一个线程可以进行当前账户增加账户余额的操作 。

等待获得锁的线程和调用await方法的线程存在本质上的不同。一旦一个线程调用await方法,他进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,他处于阻塞状态,知道另一个线程调用同一个条件上的signalAll方法时为止。

当一个线程转账时,它应该调用:sufficientFunds.signalAll()
singnAll这一调用重新激活因为这一条件儿等待的所有进程。当这些线程从等待集当中移出时,它们再次成为可运行的,调度器将再次激活他们。同时,他们将试图重新进入该对象。一旦,锁成为可用的,他们中的某个将从await调用返回,获得该锁并从阻塞的地方继续执行。
此时,线程应该再次测试该条件。由于无法确保该条件被满足——signalAll方法仅仅是通知正在等待的线程:此时可能已经满足条件,值得再次去检测该条件。

至关重要的是最终需要某个其他线程调用signalAll方法。当一个线程调用await时,他没有办法重新激活自身。 只能寄希望于其他线程。如果没有其他线程来重新激活等待的线程,他就永远不会再运行了,这将导致令人不快的死锁现象。

那我们应该何时调用signalAll方法开避免碰见阻塞呢?经验上讲,在对象的状态有利于等待线程的方向改变时调用signalAll。 例如,在我们一会最终的程序中,将会存在:当一个用户余额发生改变时,等待的线程会应该有机会检查余额(当完成转账时,调用signaAll方法)。

还要注意,调用signalAll不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问。

下面就是最终的银行代码:

public class BankTest{
    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 = new Runnable(){
                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();
        }
    }
}

public class Bank(){
    private final double[] accounts;
    private Lock bankLock; //这里申明了锁对象引用
    private Condition sufficientFunds;//这里申明了条件对象引用

    public Bank(int n,double initialBalance)
    {
        accounts = new double[n];
        Arrays.fill(accounts,initialBalance);
        bankLock = new ReentrantLock();//构造方法中实例了锁对象
        sufficientFunds = bankLock.newCondition()//构造方法中实例了条件对象
    }

    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;
            System.out.printf("%10.2f from %d to %d", amount, from, to);
            accounts[to] += amount;
            System.out.printf("Total Balance:%10.2f%n",getTotalBalance);
            sufficientFunds.signaAll(); 
        }
        finally{
            bankLock.unlock();
            }
    }

    public double getTotalBalance()
    {
        bankLock.lock();
        try{
            double sum = 0;

            for(double e :; accounts)
                sum += a;
            return sum;
        }
        finally{
            bankLock.unlock();
        }
    }

    public int size(){
        return accounts.length;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章