线程死锁研究之四
上页
前面我们已经根据死锁现象追踪到代码86的位置:
private void transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {
synchronized (fromAccount) {
synchronized (toAccount) {
fromAccount.withdraw(transferAmount);
toAccount.deposit(transferAmount);
}
}
}
从下面看出,我们是从账目数组随机选择两个帐户对象作为的fromAccount和toAccount,然后锁定它们
Account toAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS));
Account fromAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS));
那么我们只有规定顺序了之后就可以解决这个问题:
private void transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {
if (fromAccount.getNumber() > toAccount.getNumber()) {
synchronized (fromAccount) {
synchronized (toAccount) {
fromAccount.withdraw(transferAmount);
toAccount.deposit(transferAmount);
}
}
} else {
synchronized (toAccount) {
synchronized (fromAccount) {
fromAccount.withdraw(transferAmount);
toAccount.deposit(transferAmount);
}
}
}
}
其中根据getNumber大小进行转账。使用 account的 number 确保我们锁住的首先总是number最高的那个 Account对象,这样死锁就避免了。
下面是完整fixed的代码:
public class AvoidsDeadlockDemo {
private static final int NUM_ACCOUNTS = 10;
private static final int NUM_THREADS = 20;
private static final int NUM_ITERATIONS = 100000;
private static final int MAX_COLUMNS = 60;
static final Random rnd = new Random();
List<Account> accounts = new ArrayList<Account>();
public static void main(String args[]) {
AvoidsDeadlockDemo demo = new AvoidsDeadlockDemo();
demo.setUp();
demo.run();
}
void setUp() {
for (int i = 0; i < NUM_ACCOUNTS; i++) {
Account account = new Account(i, rnd.nextInt(1000));
accounts.add(account);
}
}
void run() {
for (int i = 0; i < NUM_THREADS; i++) {
new BadTransferOperation(i).start();
}
}
class BadTransferOperation extends Thread {
int threadNum;
BadTransferOperation(int threadNum) {
this.threadNum = threadNum;
}
@Override
public void run() {
for (int i = 0; i < NUM_ITERATIONS; i++) {
Account toAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS));
Account fromAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS));
int amount = rnd.nextInt(1000);
if (!toAccount.equals(fromAccount)) {
try {
transfer(fromAccount, toAccount, amount);
System.out.print(".");
} catch (OverdrawnException e) {
System.out.print("-");
}
printNewLine(i);
}
}
System.out.println("Thread Complete: " + threadNum);
}
private void printNewLine(int columnNumber) {
if (columnNumber % MAX_COLUMNS == 0) {
System.out.print("\n");
}
}
/**
* This is the crucial point here. The idea is that to avoid deadlock you need to ensure that threads can't try
* to lock the same two accounts in the same order
*/
private void transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {
if (fromAccount.getNumber() > toAccount.getNumber()) {
synchronized (fromAccount) {
synchronized (toAccount) {
fromAccount.withdraw(transferAmount);
toAccount.deposit(transferAmount);
}
}
} else {
synchronized (toAccount) {
synchronized (fromAccount) {
fromAccount.withdraw(transferAmount);
toAccount.deposit(transferAmount);
}
}
}
}
}
}
需要注意的是:只要你在同一个线程锁住两个或更多不同对象,顺序就影响着死锁的发生。
其他解决办法:显式使用并发锁ReentrantLock。
public class Account implements Lock {
private final int number;
private int balance;
private final ReentrantLock lock;
public Account(int number, int openingBalance) {
this.number = number;
this.balance = openingBalance;
this.lock = new ReentrantLock();
}
public void withDrawAmount(int amount) throws OverdrawnException {
if (amount > balance) {
throw new OverdrawnException();
}
balance -= amount;
}
public void deposit(int amount) {
balance += amount;
}
public int getNumber() {
return number;
}
public int getBalance() {
return balance;
}
// ------- Lock interface implementation
@Override
public void lock() {
lock.lock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
lock.lockInterruptibly();
}
@Override
public Condition newCondition() {
return lock.newCondition();
}
@Override
public boolean tryLock() {
return lock.tryLock();
}
@Override
public boolean tryLock(long arg0, TimeUnit arg1) throws InterruptedException {
return lock.tryLock(arg0, arg1);
}
@Override
public void unlock() {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
注意小技巧:解锁:
@Override
public void unlock() {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
这段检查调用线程是否是当前拥有锁的线程。如果错过了这行代码,你会得到以下IllegalMonitorStateException:
Exception in thread 'Thread-7' java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:155)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1260)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:460)
at threads.lock.Account.unlock(Account.java:76)
at threads.lock.TrylockDemo$BadTransferOperation.transfer(TrylockDemo.java:98)
at threads.lock.TrylockDemo$BadTransferOperation.run(TrylockDemo.java:67)
下面看看这个新的Account如何调用:
public class TrylockDemo {
private static final int NUM_ACCOUNTS = 10;
private static final int NUM_THREADS = 20;
private static final int NUM_ITERATIONS = 100000;
private static final int LOCK_ATTEMPTS = 10000;
static final Random rnd = new Random();
List<Account> accounts = new ArrayList<Account>();
public static void main(String args[]) {
TrylockDemo demo = new TrylockDemo();
demo.setUp();
demo.run();
}
void setUp() {
for (int i = 0; i < NUM_ACCOUNTS; i++) {
Account account = new Account(i, 1000);
accounts.add(account);
}
}
void run() {
for (int i = 0; i < NUM_THREADS; i++) {
new BadTransferOperation(i).start();
}
}
class BadTransferOperation extends Thread {
int threadNum;
BadTransferOperation(int threadNum) {
this.threadNum = threadNum;
}
@Override
public void run() {
int transactionCount = 0;
for (int i = 0; i < NUM_ITERATIONS; i++) {
Account toAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS));
Account fromAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS));
int amount = rnd.nextInt(1000);
if (!toAccount.equals(fromAccount)) {
boolean successfulTransfer = false;
try {
successfulTransfer = transfer(fromAccount, toAccount, amount);
} catch (OverdrawnException e) {
successfulTransfer = true;
}
if (successfulTransfer) {
transactionCount++;
}
}
}
System.out.println("Thread Complete: " + threadNum + " Successfully made " + transactionCount + " out of "
+ NUM_ITERATIONS);
}
private boolean transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {
boolean success = false;
for (int i = 0; i < LOCK_ATTEMPTS; i++) {
try {
if (fromAccount.tryLock()) {
try {
if (toAccount.tryLock()) {
success = true;
fromAccount.withDrawAmount(transferAmount);
toAccount.deposit(transferAmount);
break;
}
} finally {
toAccount.unlock();
}
}
} finally {
fromAccount.unlock();
}
}
return success;
}
}
}
尝试锁定先是从fromAccount然后到toAccount。如果这样正常,在解锁两个帐户之前进行转让。如果帐户已被锁定,那么我的tryLock()方法失败,整个事情的循环下去,再次尝试。 10000锁定尝试后,线程放弃,而忽略转让。我想,在现实世界中的应用,你想要把这个失败放到到某种形式的队列,以便它可以稍后被调出进行调查。
如下输出结果:
Thread Complete: 17 Successfully made 58142 out of 100000
Thread Complete: 12 Successfully made 57627 out of 100000
Thread Complete: 9 Successfully made 57901 out of 100000
Thread Complete: 16 Successfully made 56754 out of 100000
Thread Complete: 3 Successfully made 56914 out of 100000
Thread Complete: 14 Successfully made 57048 out of 100000
Thread Complete: 8 Successfully made 56817 out of 100000
Thread Complete: 4 Successfully made 57134 out of 100000
Thread Complete: 15 Successfully made 56636 out of 100000
Thread Complete: 19 Successfully made 56399 out of 100000
Thread Complete: 2 Successfully made 56603 out of 100000
Thread Complete: 13 Successfully made 56889 out of 100000
Thread Complete: 0 Successfully made 56904 out of 100000
Thread Complete: 5 Successfully made 57119 out of 100000
Thread Complete: 7 Successfully made 56776 out of 100000
Thread Complete: 6 Successfully made 57076 out of 100000
Thread Complete: 10 Successfully made 56871 out of 100000
Thread Complete: 11 Successfully made 56863 out of 100000
Thread Complete: 18 Successfully made 56916 out of 100000
Thread Complete: 1 Successfully made 57304 out of 100000
下面是第二个版本,使用.tryLock(LOCK_TIMEOUT, TimeUnit.MILLISECONDS)替代循环:
private boolean transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {
boolean success = false;
try {
if (fromAccount.tryLock(LOCK_TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
if (toAccount.tryLock(LOCK_TIMEOUT, TimeUnit.MILLISECONDS)) {
success = true;
fromAccount.withDrawAmount(transferAmount);
toAccount.deposit(transferAmount);
}
} finally {
toAccount.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
fromAccount.unlock();
}
return success;
}
输出:
Thread Complete: 0 Successfully made 26637 out of 100000
Thread Complete: 14 Successfully made 26516 out of 100000
Thread Complete: 3 Successfully made 26552 out of 100000
Thread Complete: 11 Successfully made 26653 out of 100000
Thread Complete: 7 Successfully made 26399 out of 100000
Thread Complete: 1 Successfully made 26602 out of 100000
Thread Complete: 18 Successfully made 26606 out of 100000
Thread Complete: 17 Successfully made 26358 out of 100000
Thread Complete: 19 Successfully made 26407 out of 100000
Thread Complete: 16 Successfully made 26312 out of 100000
Thread Complete: 15 Successfully made 26449 out of 100000
Thread Complete: 5 Successfully made 26388 out of 100000
Thread Complete: 8 Successfully made 26613 out of 100000
Thread Complete: 2 Successfully made 26504 out of 100000
Thread Complete: 6 Successfully made 26420 out of 100000
Thread Complete: 4 Successfully made 26452 out of 100000
Thread Complete: 9 Successfully made 26287 out of 100000
Thread Complete: 12 Successfully made 26507 out of 100000
Thread Complete: 10 Successfully made 26660 out of 100000
Thread Complete: 13 Successfully made 26523 out of 100000
上面的结果表明,使用一个计时器时,余额转移的成功率下降略多于25%。虽然它现在不耗费堆栈时间,它仍然是非常低效的。
java多线程
Java同步或锁
Java性能调优