Java - 可重入鎖ReentrantLock簡(jiǎn)單用法
Java 中顯示鎖的借口和類主要位于java.util.concurrent.locks下,其主要的接口和類有:
- 鎖接口Lock,其主要實(shí)現(xiàn)為ReentrantLock
- 讀寫(xiě)鎖接口ReadWriteLock,其主要實(shí)現(xiàn)為ReentrantReadWriteLock
一、接口Lock
其中顯示鎖Lock的定義為:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
其中:
- lock()/unlock() : 為獲取鎖和釋放鎖的方法,其中l(wèi)ock()會(huì)阻塞程序,直到成功的獲取鎖。
- lockInterruptibly():與lock()不同的地方是,它可以響應(yīng)程序中斷,如果被其他程序中斷了,則拋出InterruptedException。
- tryLock():嘗試獲取鎖,該方法會(huì)立即返回,并不會(huì)阻塞程序。如果獲取鎖成功則返回true,反之則返回false。
- tryLock(long time, TimeUnit unit):嘗試獲取鎖,如果能獲取鎖則直接返回true;否則阻塞等待,阻塞時(shí)長(zhǎng)由傳入的參數(shù)來(lái)決定,在等待的同時(shí)響應(yīng)程序中斷,如果發(fā)生了中斷則拋出InterruptedException;如果在等待的時(shí)間中獲取了鎖則返回true,反之返回false。
- newCondition():新建一個(gè)條件,一個(gè)Lock可以關(guān)聯(lián)多個(gè)條件。
相比synchronized,顯示鎖可以用非阻塞的方式獲取鎖,可以響應(yīng)程序中斷,可以設(shè)定程序的阻塞時(shí)間,擁有更加靈活的操作。
二、可重入鎖ReentrantLock
2.1 基本用法
ReentrantLock是Lock接口的主要實(shí)現(xiàn)類,其基本用法lock()/unlock()實(shí)現(xiàn)了與synchronized一樣的語(yǔ)義,其中包括:
- 可重入,一個(gè)線程在持有一個(gè)鎖的前提下,可以繼續(xù)獲得該鎖;
- 可以解決競(jìng)態(tài)條件問(wèn)題(臨界區(qū)資源);
- 可以保證內(nèi)存可見(jiàn)性問(wèn)題。
ReentrantLock有兩個(gè)構(gòu)造方法。
public ReentrantLock()
public ReentrantLock(boolean fair)
參數(shù)fair表示是否保證公平,在不指定的情況下默認(rèn)值為false,表示不保證公平。
公平的意思是指:等待時(shí)間最長(zhǎng)的線程優(yōu)先獲取鎖。
但是保證公平可能會(huì)影響程序的性能,在一般情況下也不需要保證公平,所以默認(rèn)值為 false 。而synchronized也是不保證公平的。
在使用顯示鎖的情況下,一定要記得調(diào)用 unlock 。一般而言,應(yīng)該將 lock 之后的代碼塊包裝在 try 語(yǔ)句中,在 finally 語(yǔ)句中釋放鎖,例如以下實(shí)現(xiàn)計(jì)數(shù)器的代碼:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by Joe on 2018/4/10.
*/
public class Counter {
private final Lock lock = new ReentrantLock();
private volatile int count;
public void incr() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
2.2 使用tryLock避免死鎖
使用tryLock()方法可以避免死鎖的發(fā)生。在持有一個(gè)鎖而嘗試獲取另外一個(gè)鎖,但是獲取不到的時(shí)候,可以釋放已持有的鎖,給其他線程獲取鎖的機(jī)會(huì),然后重試獲取所有的鎖。
接下來(lái)使用銀行之間轉(zhuǎn)賬的例子。
表示賬戶的Account類:
public class Account {
private Lock lock = new ReentrantLock();
private volatile double money;
public Account(double initialMoney) {
this.money = initialMoney;
}
public void add(double money) {
lock.lock();
try {
this.money += money;
} finally {
lock.unlock();
}
}
public void reduce(double money) {
lock.lock();
try {
this.money -= money;
} finally {
lock.unlock();
}
}
public double getMoney() {
return money;
}
void lock() {
lock.lock();
}
void unlock() {
lock.unlock();
}
boolean tryLock() {
return lock.tryLock();
}
}
Account類中的money表示當(dāng)前的余額。add/reduce用于修改余額。在賬戶之間轉(zhuǎn)賬,需要這兩個(gè)賬戶都要進(jìn)行鎖定。如果我們直接只用 lock() ,我們的代碼清單如下:
public class AccountMgr {
public static class NoEnoughMoneyException extends Exception {}
public static void transfer(Account from, Account to, double money)
throws NoEnoughMoneyException {
from.lock();
try {
to.lock();
try {
if(from.getMoney() >= money) {
from.reduce(money);
to.add(money);
} else {
throw new NoEnoughMoneyException();
}
} finally {
to.unlock();
}
} finally {
from.unlock();
}
}
}
但是這種寫(xiě)法容易發(fā)生死鎖。比如,兩個(gè)賬戶都想同時(shí)給對(duì)方進(jìn)行轉(zhuǎn)賬,并且均獲得了第一個(gè)鎖。在這種情況下就會(huì)發(fā)生死鎖。
接下來(lái)的代碼用于模擬賬戶轉(zhuǎn)賬的死鎖過(guò)程。
public static void simulateDeadLock() {
final int accountNum = 10;
final Account[] accounts = new Account[accountNum];
final Random rnd = new Random();
for(int i = 0; i < accountNum; i++) {
accounts[i] = new Account(rnd.nextInt(10000));
}
int threadNum = 100;
Thread[] threads = new Thread[threadNum];
for(int i = 0; i < threadNum; i++) {
threads[i] = new Thread() {
public void run() {
int loopNum = 100;
for(int k = 0; k < loopNum; k++) {
int i = rnd.nextInt(accountNum);
int j = rnd.nextInt(accountNum);
int money = rnd.nextInt(10);
if(i != j) {
try {
transfer(accounts[i], accounts[j], money);
System.out.println(i + "--->" + j + "轉(zhuǎn)賬成功:" + money);
} catch (NoEnoughMoneyException e) {
}
}
}
}
};
threads[i].start();
}
}
public static void main(String[] args) {
simulateDeadLock();
}
以上代碼創(chuàng)建了10個(gè)賬戶,100個(gè)線程,每個(gè)線程均循環(huán)100次,在循環(huán)中隨機(jī)挑選兩個(gè)賬戶進(jìn)行轉(zhuǎn)賬。在程序運(yùn)行多次之后你會(huì)發(fā)現(xiàn)如下圖所示的情況,程序因?yàn)榘l(fā)生死鎖陷入阻塞態(tài),無(wú)法完整執(zhí)行程序:

接下來(lái)我們使用 tryLock 書(shū)寫(xiě)一個(gè)新的方法,代碼如下所示:
public static boolean tryTransfer(Account from, Account to, double money)
throws NoEnoughMoneyException {
if (from.tryLock()) {
try {
if (to.tryLock()) {
try {
if (from.getMoney() >= money) {
from.reduce(money);
to.add(money);
} else {
throw new NoEnoughMoneyException();
}
return true;
} finally {
to.unlock();
}
}
} finally {
from.unlock();
}
}
return false;
}
嘗試獲取賬戶的鎖,如果兩個(gè)鎖都能獲取成功,則返回 true,反之則返回 false。無(wú)論鎖的獲取狀態(tài)如何,在方法體結(jié)束之后都會(huì)釋放所有的鎖。同時(shí)我們可以改造 transfer 方法來(lái)循環(huán)調(diào)用該方法以避免死鎖情況的發(fā)生,其代碼可以為:
public static void transfer(Account from, Account to, double money)
throws NoEnoughMoneyException {
boolean success = false;
do {
success = tryTransfer(from, to, money);
if (!success) {
Thread.yield();
}
} while (!success);
}