关于线程间通信问题,一般有两种方式,一是Lock接口(ReentrantLock实现类),二是synchronized关键字,这两者的基本使用在上篇已经做了详细的介绍,下面通过场景代码结合来加深理解。
假设一个场景需要用支付宝转账,首先要知道支付宝账户号及每个账户金额,还有转账余额,最后判断账户余额是否充足,再决定是否进行转账操作。基于此下面用代码来提现。
重入锁(Lock)与条件对象(Condition)
首页写一个支付宝的类,在它的构造函数需要传入的支付宝账户数以及每个账户的初始余额.,同时在构造函数中需要初始化锁对象。
public class AliPay { // 账户号集,包括转账号、接收账号 private double[] accounts; private ReentrantLock mLock; /** * 在构造函数中初始化账户号数及账户对应的余额、创建锁对象 * @param accountNum 账户数量 * @param money 每个账号对应的初始金额 */ public AliPay(int accountNum, double money) { // 初始化账户数量 accounts = new double[accountNum]; // 初始化每个账会余额 for (int i = 0; i < accounts.length; i++) { accounts[i] = money; } // 锁对象 mLock = new ReentrantLock(); } }
接下来就是转账操作了,定义转账的方法,from是转账方,to是接受方,amount是转账金额:
public void transfer(int form, int to, double amount) throws InterruptedException { mLock.lock(); try { while (accounts[form]<amount) { //阻塞当前线程,不允许转账 …… } //转账的具体操作 accounts[form]=accounts[form]-amount; accounts[to]=accounts[to]+amount; System.out.println(form+"账号向"+to+"账号已转账"+amount+"元,"+form+"账户剩余余额为:"+accounts[form]); } finally{ mLock.unlock(); } }
上面的定义,在正常情况下是可以进行转账操作,如果当转账方余额不足的情况下,上面的程序就会出现异常了,只有等待其他线程给这个转账方存入足够的钱,才可以转账成功。这就需要引入条件对象(Condition)进行处理,可以通过锁对象获取条件对象,然后通过调用条件对象的await方法,进行阻塞余额不足的情况,完整代码如下:
public class AliPay { // 账户号集,包括转账号、接收账号 private double[] accounts; private ReentrantLock mLock; private Condition mCondition; /** * 在构造函数中初始化账户号数及账户对应的余额、创建锁对象及条件对象 * @param accountNum 账户数量 * @param money 每个账号对应的初始金额 */ public AliPay(int accountNum, double money) { // 初始化账户数量 accounts = new double[accountNum]; // 初始化每个账会余额 for (int i = 0; i < accounts.length; i++) { accounts[i] = money; } // 锁对象、条件对象 mLock = new ReentrantLock(); mCondition = mLock.newCondition(); } /** * 转账操作是随时都可以进行的,但前提条件就是账户余额必须充足 * 所以在转账的过程中,需要进行条件判断 * @param form 转账方的账户(通常讲的打款方),在accounts数组中第几个账户 * @param to 接受方的账户,在accounts数组中第几个账户 * @param amount 转账金额 * * tips:在测试过程中,form与to形参不能相同 * @throws InterruptedException */ public void transfer(int form, int to, double amount) throws InterruptedException { mLock.lock(); try { while (accounts[form]<amount) { //阻塞当前线程 mCondition.await(); } //转账的具体操作 accounts[form]=accounts[form]-amount; accounts[to]=accounts[to]+amount; System.out.println(form+"账号向"+to+"账号已转账"+amount+"元,"+form+"账户剩余余额为:"+accounts[form]); //唤醒在同一条件下的所有等待线程 mCondition.signalAll(); } finally{ mLock.unlock(); } } /** * 存款操作 是随时都可以进行的,不受任何条件的影响 * 但每次存入必须去唤醒转账等待线程,以达到能及时转账 * @param to 存款方的账户,在accounts数组中第几个账户 * @param amount 存款金额 */ public void deposit(int to, double amount) { mLock.lock(); try { double original=accounts[to]; accounts[to]=original+amount; mCondition.signalAll(); System.out.println("原账号余额:"+original+",现向"+to+"账户已存入"+amount+"元,"+to+"账户当前余额为:"+accounts[to]); } finally{ mLock.unlock(); } } }
一旦一个线程调用了await方法,该线程就会进入该条件的等待集中并处于阻塞状态,直到另外一个线程调用了同一个条件(同一个Lock对象)的signalAll方法来唤醒为止。就是相当于存款线程存入金额充足时,调用了signalAll方法,就会重新激活等待的所有线程。
转账线程:
public class TransferThread extends Thread { private AliPay mAliPay; private int fromNo; private int toNo; private double amount; public TransferThread(AliPay mAliPay, int fromNo, int toNo, double amount) { super(); this.mAliPay = mAliPay; this.fromNo = fromNo; this.toNo = toNo; this.amount = amount; } public void setAmount(double amount) { this.amount = amount; } @Override public void run() { super.run(); try { mAliPay.transfer(fromNo, toNo, amount); } catch (InterruptedException e) { e.printStackTrace(); } } }
存款线程:
public class DepositThread extends Thread { private AliPay mAliPay; // 存入的账户号 private int depositNo; private double amount; public DepositThread(AliPay mAliPay, int depositNo, double amount) { super(); this.mAliPay = mAliPay; this.depositNo = depositNo; this.amount = amount; } @Override public void run() { super.run(); mAliPay.deposit(depositNo, amount); } }
调用及输入测试结果:
public class AliPayTest { public static void main(String[] args) { AliPay aliPay = new AliPay(2, 100); // 由于转账余额大于账户余额,转账线程会处于阻塞状态,直到存款线程唤醒,即可成功转款 new TransferThread(aliPay, 0, 1, 178).start(); // 存款并唤醒等待线程 new DepositThread(aliPay, 0, 88).start(); } }
原账号余额:100.0,现向0账户已存入88.0元,0账户当前余额为:188.0 0账号向1账号已转账178.0元,0账户剩余余额为:10.0
下面是Condition条件对象提供的三个方法,结合上面的代码能更好的理解:
await: 导致当前线程进入等待,直到其他线程调用了同个条件对象Condition的signal方法或signalAll方法来唤醒该线程。
signal:唤醒在此Lock对象上的单个等待线程。如果所有线程都在改Lock对象上等待,则会选择唤醒其中一个线程。选择是任意性。只有当前线程放弃对该Lock对象的锁定后(即调用了await方法),才可以执行被唤醒的线程。
signalAll:唤醒在此Lock对象上等待的所有线程。
同步方法
Lock接口和条件对象(Condition)提供了高度锁定控制,功能相对丰富,然而很多时候,开发中并不希望有这样的控制,希望能简单一点实现同步锁机制来达到需求,此时可以使用java内置锁机制synchronized,在java中一个对象都有一个内部锁。如果一个方法使用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获取内部的对象锁。同步方法声明如下:
public synchronized void method(){ }
等价于
Lock mLock=new ReentrantLock(); public void method(){ this.lock.lock(); try{ }finally{ this.lock.unlock(); }
对于支付宝转账的例子,我们可以AliPay类下的转账方法(transfer)和存款方法(deposit)声明synchronized,内部对象锁只有一个相关条件,wait方法将一个线程添加到等待集中,通过notifyAll或notify方法解除唤醒等待线程的阻塞状态。也就是说wait方法相当于条件对象Condition的await方法,notifyAll等价于Condition的signalAll。代码如下:
public synchronized void transfer(int form, int to, double amount) throws InterruptedException { while (accounts[form] < amount) { // 阻塞当前线程 wait(); } // 转账的具体操作 accounts[form] = accounts[form] - amount; accounts[to] = accounts[to] + amount; System.out.println(form + "账号向" + to + "账号已转账" + amount + "元," + form + "账户剩余余额为:" + accounts[form]); // 唤醒在同一条件下的所有等待线程 notifyAll(); } public synchronized void deposit(int to, double amount) { double original = accounts[to]; accounts[to] = original + amount; notifyAll(); System.out.println("原账号余额:" + original + ",现向" + to + "账户已存入" + amount + "元," + to + "账户当前余额为:" + accounts[to]);
调用和输出结果与使用ReentrantLock是一样的,可以看出synchronized关键字来处理同等问题代码简洁了很多。需要理解的是在java中每个对象都有一个内部锁,并且该锁有一个内部条件。有该锁来管理那些试图进入synchronized方法的线程,由该锁中条件来管理那些调用的wait的线程。
上面的概念可以来一个比喻来理解:在公共场所一群人排队如厕,如果一个人想如厕,必须先获取厕所门锁才能进入,也就是说必须等待前一个人完成整个如厕过程后,才能拿到这把锁进入厕所。我们把一个对象比作公共厕所,厕所门锁比作对象内部锁,把需要进入如厕等待的人,看成一个等待线程集,单个人看成一个线程,而厕所门锁相当于wait方法它管理整个排队等待的人,我们可以想象下,如果没有门锁,在众多人都需要如厕,一下子都冲进厕所,是不是破坏整个如厕流程了,相互之间产生了竞争关系,这样就导致了一些人无法完成如厕,后果可想而知,便便可能露外了……
同步代码块
由于java中每个对象都有一个锁,线程可以调用同步方法来获取对象锁。还有同步代码块也实现上面需求:
synchronized(obj){ }其obj代表一把锁,obj指的是一个对象,如果调用了同步代码块所在的方法就可以获取obj锁对象。代码如下:
public void transfer(int form, int to, double amount) throws InterruptedException { synchronized (this) { while (accounts[form] < amount) { // 阻塞当前线程 wait(); } // 转账的具体操作 accounts[form] = accounts[form] - amount; accounts[to] = accounts[to] + amount; System.out.println(form + "账号向" + to + "账号已转账" + amount + "元," + form + "账户剩余余额为:" + accounts[form]); } } public void deposit(int to, double amount) { synchronized (this) { double original = accounts[to]; accounts[to] = original + amount; notifyAll(); System.out.println("原账号余额:" + original + ",现向" + to + "账户已存入" + amount + "元," + to + "账户当前余额为:" + accounts[to]); } }
小结:如果使用同步方法实现需求,尽量使用同步方法,这样可以减少代码量,减少出错的概率。如果需要特别的需求,比如要中断锁、判断是否获取锁成功等等,可以使用Lock/Condition条件对象提供的独有的特性,这时可以选择使用Lock/Condition。