Java多线程 - 同步synchronized与ReentrantLock(二)

Java多线程之同步(一)

关于线程间通信问题,一般有两种方式,一是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。





猜你喜欢

转载自blog.csdn.net/hzw2017/article/details/80640426