银行转账是一道典型的考察多线程的题目,该题目主要考察对锁的对象的选择以及对死锁的了解,如何避免死锁。
重要!重要!重要!文中所有方法,经测试均未实现安全的转账功能(包括设置同步方法),本文只作为自己的学习记录,所以初学者慎重考虑,以免被带入歧途!!!
另外,期望有大佬能指出不能安全转账的问题所在!不胜感激!!!
当然,初学者也可以发表自己的意见,大家一起讨论。
为方便阅读,以下所有代码可能会删除一部分业务逻辑
完整代码
如果我们简单的将转账方法设置为同步方法。肯定可以实现正确的同步。但这样有严重的性能问题,当用户A向用户B转账的时候,用户C想要转账给用户D,必须等待用户A转账完毕才行。本来两者没有任何的数据依赖关系,强行实现同步,导致每次只能有一个用户进行转账操作。这肯定不是我们想要的。
代码:
public synchronized void transfer(String from, String to, int money)
{
//转账逻辑
}
实际上我们在转账的时候只是不允许别人对转账的这两个账户进行操作,否则就有可能出现数据的错误。那我们是不是可以对账户上锁,只要保证其他用户无法对转账的两个账户操作即可。
代码如下:
public synchronized boolean transferAccounts(String accountA, String transferAccount, int money) {
UserAccount account = BankLock.getUserAccount(accountA);
boolean success = false;
UserAccount you = BankLock.getUserAccount(transferAccount);
if(account.getId() > you.getId())
{
synchronized (you) {
synchronized (account) {
success = transferUtil.transfer(account, you, money);
}
}
}
else
{
synchronized (account) {
synchronized (you) {
success = transferUtil.transfer(account, you, money);
}
}
}
return success;
}
当我们对账户上锁的时候,就会引出来另一个问题,死锁。注意到上述的代码if和else中的代码逻辑基本相同,只是加锁顺序不同。该逻辑即为了避免死锁问题。
避免死锁的方法:
- 控制锁的顺序,我们以一种固定的顺序去加锁,比如上述的代码,比较两个账户id,每次都是先锁id值较小的账户。
- 限时获取锁,使用Lock锁,当一定的时间内无法获取到想要的锁。则释放已经获取到的锁,等待一段时间后再重新获取。
方法2代码如下:
public boolean transferAccounts(String accountA, String transferAccount, int money) {
UserAccount account = accountDao.getAccount(accountA);
Lock lockA = BankLock.getLock(account);
int times = 0;
boolean success = false;
UserAccount you = accountDao.getAccount(transferAccount);
lockA.lock();
boolean tryLockB = false;
try
{
Lock lockB = BankLock.getLock(you);
//尝试三次,若还未获取成功,则转账失败退出。
while(times++ < 3)
{
try {
tryLockB = lockB.tryLock(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(!tryLockB)
{
Condition condition = BankLock.getLockCondition(account);
//1s内沒有获取到锁B,先释放锁A等待1秒,然后重新执行
try {
condition.await(1,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else
{
break;
}
}
if(tryLockB)
{
try {
success = transferUtil.transfer(account, you, money);
} finally {
lockB.unlock();
}
}
}
finally
{
Condition condition = BankLock.getLockCondition(account);
condition.signalAll();
lockA.unlock();
}
return success;
}
下边给出一段测试代码:
public void testTransfer1()
{
for(int i = 0; i < 10; i++)
{
new Thread(new Runnable() {
@Override
public void run() {
while(true)
{
Random random = new Random();
//最多操作三个账户
int accountA = (random.nextInt(1000) % 3);
int accountB = (random.nextInt(1000) % 3);
if (accountA == accountB)
continue;
int money = random.nextInt(1000);
System.out.println(accountA + " to " + accountB + " : " + money + " " + Thread.currentThread().getName());
amountService.transferAccounts(String.valueOf(accountA), String.valueOf(accountB), money);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
},"thread-" + i).start();
}
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
附1:一段错误的代码示范,以下代码可能引起死锁。该段代码主要引起死锁的原因是对await方法不熟悉导致的,知其然不知其所以然。调用await方法会释放锁等待,当有其他线程调用signal或signalAll方法时,会将该线程重新放到阻塞队列获取锁,不需要显示获取锁,所以最后该线程仍然需要释放锁。
代码如下:
public boolean transferAccounts(String accountA, String transferAccount, int money) {
UserAccount account = accountDao.getAccount(accountA);
Lock lockA = BankLock.getLock(account);
int times = 0;
boolean success = false;
UserAccount you = accountDao.getAccount(transferAccount);
while(times++ < 3)
{
lockA.lock();
boolean tryLockB = false;
try
{
Lock lockB = BankLock.getLock(you);
try {
tryLockB = lockB.tryLock(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(tryLockB)
{
try {
//转账
break;
} finally {
lockB.unlock();
}
}
}
finally
{
if(tryLockB)
{
Condition condition = BankLock.getLockCondition(account);
condition.signalAll();
lockA.unlock();
}
else
{
Condition condition = BankLock.getLockCondition(account);
//1s内沒有获取到锁B,先释放锁A等待1秒,然后重新执行
try {
condition.await(1,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
return success;
}