從Java 5之後,在java.util.concurrent.locks包下提供了另外一種方式來實現同步訪問,那就是Lock。
1.Lock
首先要說明的就是Lock,通過查看Lock的源碼可知,Lock是一個接口:
1 2 3 4 5 6 7 8public
interface
Lock {
void
lock();
void
lockInterruptibly()
throws
InterruptedException;
boolean
tryLock();
boolean
tryLock(
long
time, TimeUnit unit)
throws
InterruptedException;
void
unlock();
Condition newCondition();
}
下面來逐個講述Lock接口中每個方法的使用,lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的。unLock()方法是用來釋放鎖的。newCondition()這個方法暫且不在此講述,會在後面的線程協作一文中講述。
在Lock中聲明了四個方法來獲取鎖,那麼這四個方法有何區別呢?
首先lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。
由於在前面講到如果采用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。通常使用Lock來進行同步的話,是以下面這種形式去使用的:
1 2 3 4 5 6 7 8 9Lock lock = ...;
lock.lock();
try
{
//處理任務
}
catch
(Exception ex){
}
finally
{
lock.unlock();
//釋放鎖
}
tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。
所以,一般情況下通過tryLock來獲取鎖時是這樣使用的:
1 2 3 4 5 6 7 8 9 10Lock lock = ...;
if
(lock.tryLock()) {
try
{
//處理任務
}
catch
(Exception ex){
}
finally
{
lock.unlock();
//釋放鎖
}
}
else
{
//如果不能獲取鎖,則直接做其他事情
}
lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態。也就使說,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那麼對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。
由於lockInterruptibly()的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在try塊中或者在調用lockInterruptibly()的方法外聲明拋出InterruptedException。
因此lockInterruptibly()一般的使用形式如下:
1 2 3 4 5 6 7 8 9public
void
method()
throws
InterruptedException {
lock.lockInterruptibly();
try
{
//.....
}
finally
{
lock.unlock();
}
}
注意,當一個線程獲取了鎖之後,是不會被interrupt()方法中斷的。因為本身在前面的文章中講過單獨調用interrupt()方法不能中斷正在運行過程中的線程,只能中斷阻塞過程中的線程。
因此當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的。
而用synchronized修飾的話,當一個線程處於等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。
2.ReentrantLock
ReentrantLock,意思是“可重入鎖”,關於可重入鎖的概念在下一節講述。ReentrantLock是唯一實現了Lock接口的類,並且ReentrantLock提供了更多的方法。下面通過一些實例看具體看一下如何使用ReentrantLock。
例子1,lock()的正確使用方法
1 2 3 4 5 6 7 8 9 10public
class
Test {
private
ArrayList arrayList =
new
ArrayList();
public
static
void
main(String[] args) {
final
Test test =
new
Test();
new
Thread(){
public
void
run() {
test.insert(Thread.currentThread());
};
}.start();
new
Thread(){
public
void
run() {
test.insert(Thread.currentThread());
};
}.start();
}
public
void
insert(Thread thread) {
Lock lock =
new
ReentrantLock();
//注意這個地方
lock.lock();
try
{
System.out.println(thread.getName()+
"得到了鎖"
);
for
(
int
i=
0
;i<
5
;i++) {
arrayList.add(i);
}
}
catch
(Exception e) {
// TODO: handle exception
}
finally
{
System.out.println(thread.getName()+
"釋放了鎖"
);
lock.unlock();
}
}
}
各位朋友先想一下這段代碼的輸出結果是什麼?
Thread-0得到了鎖 Thread-1得到了鎖 Thread-0釋放了鎖 Thread-1釋放了鎖
也許有朋友會問,怎麼會輸出這個結果?第二個線程怎麼會在第一個線程釋放鎖之前得到了鎖?原因在於,在insert方法中的lock變量是局部變量,每個線程執行該方法時都會保存一個副本,那麼理所當然每個線程執行到lock.lock()處獲取的是不同的鎖,所以就不會發生沖突。
知道了原因改起來就比較容易了,只需要將lock聲明為類的屬性即可。
1 2 3 4 5 6 7 8 9 10public
class
Test {
private
ArrayList arrayList =
new
ArrayList();
private
Lock lock =
new
ReentrantLock();
//注意這個地方
public
static
void
main(String[] args) {
final
Test test =
new
Test();
new
Thread(){
public
void
run() {
test.insert(Thread.currentThread());
};
}.start();
new
Thread(){
public
void
run() {
test.insert(Thread.currentThread());
};
}.start();
}
public
void
insert(Thread thread) {
lock.lock();
try
{
System.out.println(thread.getName()+
"得到了鎖"
);
for
(
int
i=
0
;i<
5
;i++) {
arrayList.add(i);
}
}
catch
(Exception e) {
// TODO: handle exception
}
finally
{
System.out.println(thread.getName()+
"釋放了鎖"
);
lock.unlock();
}
}
}
這樣就是正確地使用Lock的方法了。
例子2,tryLock()的使用方法
1 2 3 4 5 6 7 8 9 10public
class
Test {
private
ArrayList arrayList =
new
ArrayList();
private
Lock lock =
new
ReentrantLock();
//注意這個地方
public
static
void
main(String[] args) {
final
Test test =
new
Test();
new
Thread(){
public
void
run() {
test.insert(Thread.currentThread());
};
}.start();
new
Thread(){
public
void
run() {
test.insert(Thread.currentThread());
};
}.start();
}
public
void
insert(Thread thread) {
if
(lock.tryLock()) {
try
{
System.out.println(thread.getName()+
"得到了鎖"
);
for
(
int
i=
0
;i<
5
;i++) {
arrayList.add(i);
}
}
catch
(Exception e) {
// TODO: handle exception
}
finally
{
System.out.println(thread.getName()+
"釋放了鎖"
);
lock.unlock();
}
}
else
{
System.out.println(thread.getName()+
"獲取鎖失敗"
);
}
}
}
輸出結果:
Thread-0得到了鎖 Thread-1獲取鎖失敗 Thread-0釋放了鎖
例子3,lockInterruptibly()響應中斷的使用方法:
1 2 3 4 5 6 7 8 9 10public
class
Test {
private
Lock lock =
new
ReentrantLock();
public
static
void
main(String[] args) {
Test test =
new
Test();
MyThread thread1 =
new
MyThread(test);
MyThread thread2 =
new
MyThread(test);
thread1.start();
thread2.start();
try
{
Thread.sleep(
2000
);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
thread2.interrupt();
}
public
void
insert(Thread thread)
throws
InterruptedException{
lock.lockInterruptibly();
//注意,如果需要正確中斷等待鎖的線程,必須將獲取鎖放在外面,然後將InterruptedException拋出
try
{
System.out.println(thread.getName()+
"得到了鎖"
);
long
startTime = System.currentTimeMillis();
for
( ; ;) {
if
(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
break
;
//插入數據
}
}
finally
{
System.out.println(Thread.currentThread().getName()+
"執行finally"
);
lock.unlock();
System.out.println(thread.getName()+
"釋放了鎖"
);
}
}
}
class
MyThread
extends
Thread {
private
Test test =
null
;
public
MyThread(Test test) {
this
.test = test;
}
@Override
public
void
run() {
try
{
test.insert(Thread.currentThread());
}
catch
(InterruptedException e) {
System.out.println(Thread.currentThread().getName()+
"被中斷"
);
}
}
}
運行之後,發現thread2能夠被正確中斷。
3.ReadWriteLock
ReadWriteLock也是一個接口,在它裡面只定義了兩個方法:
1 2 3 4 5 6 7 8 9 10public
interface
ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing.
*/
Lock writeLock();
}
一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將文件的讀寫操作分開,分成2個鎖來分配給線程,從而使得多個線程可以同時進行讀操作。下面的ReentrantReadWriteLock實現了ReadWriteLock接口。
4.ReentrantReadWriteLock
ReentrantReadWriteLock裡面提供了很多豐富的方法,不過最主要的有兩個方法:readLock()和writeLock()用來獲取讀鎖和寫鎖。
下面通過幾個例子來看一下ReentrantReadWriteLock具體用法。
假如有多個線程要同時進行讀操作的話,先看一下synchronized達到的效果:
1 2 3 4 5 6 7 8 9 10public
class
Test {
private
ReentrantReadWriteLock rwl =
new
ReentrantReadWriteLock();
public
static
void
main(String[] args) {
final
Test test =
new
Test();
new
Thread(){
public
void
run() {
test.get(Thread.currentThread());
};
}.start();
new
Thread(){
public
void
run() {
test.get(Thread.currentThread());
};
}.start();
}
public
synchronized
void
get(Thread thread) {
long
start = System.currentTimeMillis();
while
(System.currentTimeMillis() - start <=
1
) {
System.out.println(thread.getName()+
"正在進行讀操作"
);
}
System.out.println(thread.getName()+
"讀操作完畢"
);
}
}
這段程序的輸出結果會是,直到thread1執行完讀操作之後,才會打印thread2執行讀操作的信息。
Thread-0正在進行讀操作而改成用讀寫鎖的話:
1 2 3 4 5 6 7 8 9 10public
class
Test {
private
ReentrantReadWriteLock rwl =
new
ReentrantReadWriteLock();
public
static
void
main(String[] args) {
final
Test test =
new
Test();
new
Thread(){
public
void
run() {
test.get(Thread.currentThread());
};
}.start();
new
Thread(){
public
void
run() {
test.get(Thread.currentThread());
};
}.start();
}
public
void
get(Thread thread) {
rwl.readLock().lock();
try
{
long
start = System.currentTimeMillis();
while
(System.currentTimeMillis() - start <=
1
) {
System.out.println(thread.getName()+
"正在進行讀操作"
);
}
System.out.println(thread.getName()+
"讀操作完畢"
);
}
finally
{
rwl.readLock().unlock();
}
}
}
此時打印的結果為:
Thread-0正在進行讀操作 Thread-0正在進行讀操作 Thread-1正在進行讀操作 Thread-0正在進行讀操作 Thread-1正在進行讀操作 Thread-0正在進行讀操作 Thread-1正在進行讀操作 Thread-1正在進行讀操作 Thread-1正在進行讀操作 Thread-1正在進行讀操作 Thread-1正在進行讀操作 Thread-1正在進行讀操作 Thread-0正在進行讀操作 Thread-0正在進行讀操作 Thread-0正在進行讀操作 Thread-0正在進行讀操作 Thread-1正在進行讀操作 Thread-1正在進行讀操作 Thread-1正在進行讀操作 Thread-1正在進行讀操作 Thread-0正在進行讀操作 Thread-1正在進行讀操作 Thread-1正在進行讀操作 Thread-0正在進行讀操作 Thread-1正在進行讀操作 Thread-1正在進行讀操作 Thread-0正在進行讀操作 Thread-1正在進行讀操作 Thread-1正在進行讀操作 Thread-1正在進行讀操作 Thread-0正在進行讀操作 Thread-1正在進行讀操作 Thread-1正在進行讀操作 Thread-0正在進行讀操作 Thread-1正在進行讀操作 Thread-0正在進行讀操作 Thread-1正在進行讀操作 Thread-0正在進行讀操作 Thread-1正在進行讀操作 Thread-0正在進行讀操作 Thread-1正在進行讀操作 Thread-0正在進行讀操作 Thread-1正在進行讀操作 Thread-0正在進行讀操作 Thread-1正在進行讀操作 Thread-0正在進行讀操作 Thread-1正在進行讀操作 Thread-0讀操作完畢 Thread-1讀操作完畢
說明thread1和thread2在同時進行讀操作。
這樣就大大提升了讀操作的效率。
不過要注意的是,如果有一個線程已經占用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。
如果有一個線程已經占用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。
關於ReentrantReadWriteLock類中的其他方法感興趣的朋友可以自行查閱API文檔