Java - 可重入鎖ReentrantLock簡單用法
Java 中顯示鎖的借口和類主要位于java.util.concurrent.locks
下,其主要的接口和類有:
- 鎖接口Lock,其主要實現(xiàn)為ReentrantLock
- 讀寫鎖接口ReadWriteLock,其主要實現(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()會阻塞程序盯漂,直到成功的獲取鎖。
- lockInterruptibly():與lock()不同的地方是储矩,它可以響應(yīng)程序中斷,如果被其他程序中斷了褂乍,則拋出InterruptedException持隧。
- tryLock():嘗試獲取鎖,該方法會立即返回逃片,并不會阻塞程序屡拨。如果獲取鎖成功則返回true,反之則返回false题诵。
- tryLock(long time, TimeUnit unit):嘗試獲取鎖洁仗,如果能獲取鎖則直接返回true;否則阻塞等待性锭,阻塞時長由傳入的參數(shù)來決定赠潦,在等待的同時響應(yīng)程序中斷,如果發(fā)生了中斷則拋出InterruptedException草冈;如果在等待的時間中獲取了鎖則返回true她奥,反之返回false。
- newCondition():新建一個條件怎棱,一個Lock可以關(guān)聯(lián)多個條件哩俭。
相比synchronized,顯示鎖可以用非阻塞的方式獲取鎖拳恋,可以響應(yīng)程序中斷凡资,可以設(shè)定程序的阻塞時間,擁有更加靈活的操作谬运。
二隙赁、可重入鎖ReentrantLock
2.1 基本用法
ReentrantLock是Lock接口的主要實現(xiàn)類,其基本用法lock()/unlock()
實現(xiàn)了與synchronized
一樣的語義梆暖,其中包括:
- 可重入伞访,一個線程在持有一個鎖的前提下,可以繼續(xù)獲得該鎖轰驳;
- 可以解決競態(tài)條件問題(臨界區(qū)資源)厚掷;
- 可以保證內(nèi)存可見性問題。
ReentrantLock有兩個構(gòu)造方法级解。
public ReentrantLock()
public ReentrantLock(boolean fair)
參數(shù)fair表示是否保證公平冒黑,在不指定的情況下默認值為false,表示不保證公平勤哗。
公平的意思是指:等待時間最長的線程優(yōu)先獲取鎖薛闪。
但是保證公平可能會影響程序的性能,在一般情況下也不需要保證公平俺陋,所以默認值為 false 豁延。而synchronized也是不保證公平的昙篙。
在使用顯示鎖的情況下,一定要記得調(diào)用 unlock 诱咏。一般而言苔可,應(yīng)該將 lock 之后的代碼塊包裝在 try 語句中,在 finally 語句中釋放鎖袋狞,例如以下實現(xiàn)計數(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ā)生焚辅。在持有一個鎖而嘗試獲取另外一個鎖,但是獲取不到的時候苟鸯,可以釋放已持有的鎖同蜻,給其他線程獲取鎖的機會,然后重試獲取所有的鎖早处。
接下來使用銀行之間轉(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表示當前的余額绵咱。add/reduce用于修改余額佛寿。在賬戶之間轉(zhuǎ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();
}
}
}
但是這種寫法容易發(fā)生死鎖桃序。比如,兩個賬戶都想同時給對方進行轉(zhuǎn)賬烂瘫,并且均獲得了第一個鎖媒熊。在這種情況下就會發(fā)生死鎖。
接下來的代碼用于模擬賬戶轉(zhuǎn)賬的死鎖過程坟比。
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個賬戶泛释,100個線程,每個線程均循環(huán)100次温算,在循環(huán)中隨機挑選兩個賬戶進行轉(zhuǎn)賬。在程序運行多次之后你會發(fā)現(xiàn)如下圖所示的情況间影,程序因為發(fā)生死鎖陷入阻塞態(tài)注竿,無法完整執(zhí)行程序:
![死鎖.png-29.3kB](http://static.zybuluo.com/ZzzJoe/l251yubvz3bjjuv6263og892/%E6%AD%BB%E9%94%81.png)
接下來我們使用 tryLock 書寫一個新的方法,代碼如下所示:
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;
}
嘗試獲取賬戶的鎖魂贬,如果兩個鎖都能獲取成功巩割,則返回 true,反之則返回 false付燥。無論鎖的獲取狀態(tài)如何宣谈,在方法體結(jié)束之后都會釋放所有的鎖。同時我們可以改造 transfer 方法來循環(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);
}