今天在网上看到一段代码,是模拟银行转账的,如何保证多次转账并发执行的时候,转出账户和转入账户的金额一致。
代码可谓巧妙绝伦!先看代码:
public class MyLock {
public static void main(String[] args) {
// 模拟转出和转入账户
Account src = new Account(100000);
Account target = new Account(100000);
// 设置倒计时
CountDownLatch countDownLatch = new CountDownLatch(99999);
for (int i = 0; i < 99999; i++) {
Thread t1 = new Thread(() -> {
// 每次转1元钱,共转9999次
src.transactionToTarget(1, target);
countDownLatch.countDown();
});
t1.setName("Thread: " + i);
t1.start();
}
try {
// 等待所有线程执行完毕
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印账户余额
System.out.println("src: " + src.getBanalce());
System.out.println("target: " + target.getBanalce());
}
/**
* 账户类
*/
static class Account {
// 余额
private Integer banalce;
public Account(Integer banalce) {
this.banalce = banalce;
}
/**
* 转账
*/
public void transactionToTarget(Integer money, Account target) {
Allocator.getInstance().apply(this, target);
this.banalce = this.banalce - money;
target.setBanalce(target.getBanalce() + money);
Allocator.getInstance().release(this, target);
}
public Integer getBanalce() {
return banalce;
}
public void setBanalce(Integer banalce) {
this.banalce = banalce;
}
}
/**
* 账户管理器
*/
static class Allocator {
private Allocator() {
}
// 账户锁
private List<Account> locks = new ArrayList<>();
/**
* 为转出账户和转入账户申请锁。
*
* @param src
* @param target
*/
public synchronized void apply(Account src, Account target) {
while (locks.contains(src) || locks.contains(target)) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
locks.add(src);
locks.add(target);
}
/**
* 释放锁。
*
* @param src
* @param target
*/
public synchronized void release(Account src, Account target) {
locks.remove(src);
locks.remove(target);
this.notifyAll();
}
public static Allocator getInstance() {
return AllocatorSingle.install;
}
static class AllocatorSingle {
public static Allocator install = new Allocator();
}
}
}
有几个关键点:
1、为了在转账前,同时获得转出账户和转入账户的锁,设计了Allocator这个账户的管理器。
它就像一个账户管理员,拿着所有账户的账本。当有申请者来申请账户锁的时候,他会控制转入账户和转出账户同时存在;如果有其他线程当前正在操作这两个账户(正在执行账户的转入或转出),则当前线程就开始wait;否则,就让当前线程获得这两个账户的锁。
2、上面的代码里,wait放在了while里,而不能改成if!
原因:
模拟执行:假设有A,B,C三个线程,A线程先执行apply()方法,while检查为false,成功获得锁后开始执行。
然后B线程进入apply()方法,这时因为A线程在持有锁,所以while检查为true,开始进入wait等待。
此时C线程也进入apply()方法,跟B线程一样,也进入wait等待。
过了一会,A线程的转账执行完成,在成功release锁以后,通过notifyAll(),B和C线程同时被唤醒。
在同时被唤醒的两个线程B和C里,只会有一个线程成功抢夺资源(Allocator的单实例对象)成功,
(此处有一个疑问,如果D线程在apply()外等待,线程B、C、D会同时争夺资源呢,还是B和C先决胜出一个,再和D争夺资源?)
假设此处C成功抢夺了资源,while检查为false,便成功获得了锁,执行完apply()方法,开始执行转账操作。
(如果此处的while改成if:因为C执行完apply()方法后,释放了apply()方法的操作权,
如果此时B趁虚而入,被notify()的线程B不会再进行if判断,而是直接开始执行加锁执行(导致locks对象里有四个对象),
这样就会导致锁的混乱和失败,造成转账操作的非原子性。这便是while不能改成if的根本原因了!)
线程C执行完毕后,释放锁,B便开始执行了。
所以,几乎所有的java书籍都会建议开发者永远都要把wait()放到循环语句里面。
这是前车之鉴,不是没有道理的。
3、作者刻意把Account#transactionToTarget()方法做成非syncronized的方法,以防止同一时刻只会发生一次转账操作,可谓用心良苦!
4、wait()和notify()、notifyAll()方法,一般在synchcronized的方法内或被synchcronized锁定的代码块里调用执行;并且wait()方法的执行要放置在while(条件)里。