Java并發(fā)編程(六)---lock
前言
前面幾篇文章,我們學(xué)習(xí)了synchronized的相關(guān)知識,以及死鎖的發(fā)生條件以及避免的方式,其中有一種破壞死鎖的方式就是破壞不可搶占條件,通過synchronzied不能實現(xiàn)的,因為synchronized在申請資源的時候,如果申請不到就只能進(jìn)入阻塞狀態(tài),啥都干不了,也不能中斷。所以只能通過本期的主角lock
來處理。
lock 與synchronized 的區(qū)別
上面說可以通過lock來破壞不可搶占的條件,那么lock為啥可以支持呢?因為lock鎖有如下三個特性:
//能夠響應(yīng)中斷
void lockInterruptibly() throws InterruptedException;
//能夠響應(yīng)超時
boolean tryLock(long time, TimeUnit unit) throws InterruptedException
//能夠非阻塞的獲取鎖
boolean tryLock()
能夠響應(yīng)中斷
synchronized的問題是,持有鎖A后,如果嘗試獲取鎖B失敗,那么線程就進(jìn)入
阻塞狀態(tài),一旦發(fā)生死鎖,就沒有任何機會來喚醒阻塞的線程。如果阻塞狀態(tài)的
線程能夠響應(yīng)中斷信號,也就是說當(dāng)我們給阻塞線程發(fā)送中斷信號的時候,能夠喚醒它,
那么它就有機會釋放曾經(jīng)持有的鎖A。
支持超時
如果線程在一段時間之內(nèi)沒有獲取到鎖,不是進(jìn)入阻塞狀態(tài),而是返回一個錯誤,
那么這個線程也有機會釋放曾經(jīng)持有的鎖,
非阻塞地獲取鎖
通過調(diào)用tryLock()方法,如果返回true,則表示獲取到鎖,如果返回false,則表示獲取鎖失敗,不過其并不會進(jìn)入阻塞狀態(tài),而是直接返回。
Lock的實現(xiàn)原理簡介
典型的lock的使用如下所示:
public class LockTest2 {
final Lock lock = new ReentrantLock();
int value = 0;
public void addOne() {
lock.lock();
try {
value + = 1;
} finally {
lock.unlock();
}
}
}
Java SDK里面的ReentrantLock,
內(nèi)部持有一個volatile的成員變量state,
獲取鎖的時候,會讀寫state的值,解鎖的時候也會讀寫state的值,
也就是說在執(zhí)行value+=1之前,會讀寫一次volatile變量state,在執(zhí)行value+=1之后
又讀寫了一次volatile變量state。
根據(jù)Happens-Before規(guī)則:
順序性規(guī)則:對于線程T1 value+=1 Happens-Before 釋放鎖的操作 unlock()
valatile變量規(guī)則:由于線程T1獲取鎖之后state為1,之后釋放會后才會變成0,T2回去鎖必須先讀取state,所以線程T1的unlock()操作Happens-Before線程T2的lock()操作。
根據(jù)傳遞性:所以線程T1的value+=1 Happens-Before 線程T2的lock()操作
第一個lock的實例
public class LockTest {
public static void main(String[] args) {
MyThreadService myThreadService = new MyThreadService();
for (int i = 0; i < 5; i++) {
MyThread myThread = new MyThread("線程" + i, myThreadService);
myThread.start();
}
}
static class MyThread extends Thread {
MyThreadService myThreadService = null;
public MyThread(String name, MyThreadService myThreadService) {
super(name);
this.myThreadService = myThreadService;
}
@Override
public void run() {
myThreadService.printThread();
}
}
}
public class MyThreadService {
final Lock lock = new ReentrantLock();
public void printThread() {
lock.lock();
try {
for (int i = 0; i < 5; i++) {
System.out.println("*******" + Thread.currentThread().getName() + (" " + i));
}
} finally {
lock.unlock();
}
}
}
運行結(jié)果是:
從如上運行結(jié)果可以看出,每個線程順序執(zhí)行,lock 起到了鎖的作用,被鎖住的代碼塊同一時刻只有一個線程在執(zhí)行。
Condition
利用synchronized關(guān)鍵字與wait()和notify/notifyAll() 方法結(jié)合實現(xiàn)等待/通知截止機制,同樣的運用ReentrantLock類與Condition接口與newCondition()方法同樣可以實現(xiàn)等待/通知機制。一個lock對象可以創(chuàng)建多個Condition實例,線程對象可以注冊在制定的Conditon中,從而可以選擇性的進(jìn)行線程通知,在調(diào)度線程上更加靈活。
notifyAll() 會通知鎖對象上面所有等待的線程,效率比較低,而Condition實例上的signAll()方法只會喚醒注冊在該Condition實例中的所有等待線程。不會通知鎖對象上其余Condition實例中的等待線程。
下面我們通過一個程序來看看。
public class ConditionTest {
public static void main(String[] args) throws InterruptedException {
MyThreadConditionService myThreadConditionService = new MyThreadConditionService();
MyThread myThread = new MyThread(“線程A”, myThreadConditionService);
myThread.start();
Thread.sleep(3000);
myThreadConditionService.signal();
}
static class MyThread extends Thread {
MyThreadConditionService myThreadConditionService = null;
public MyThread(String name, MyThreadConditionService myThreadConditionService) {
super(name);
this.myThreadConditionService = myThreadConditionService;
}
@Override
public void run() {
myThreadConditionService.await();
}
}
static class MyThreadConditionService {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void await() {
lock.lock();
try {
System.out.println("await的時間是="+System.currentTimeMillis());
//等待
condition.await();
System.out.println("******這是await之后的代碼,signal之后才會執(zhí)行");
} catch (InterruptedException exception) {
} finally {
lock.unlock();
}
}
public void signal() {
lock.lock();
try {
System.out.println("signal的時間是="+System.currentTimeMillis());
condition.signal();
Thread.sleep(3000);
System.out.println("**********這是signal之后的代碼");
} catch (InterruptedException exception) {
} finally {
lock.unlock();
}
}
}
}
程序運行結(jié)果:
如上程序,我們定義了await()方法,用于調(diào)用condition.await();,定義了signal()方法用于調(diào)用condition.signal(),我們可以看到await()的執(zhí)行時間跟signal()執(zhí)行時間相差了3秒,而調(diào)用condition.signal()方法之后,執(zhí)行完其后續(xù)的代碼之后。立馬去執(zhí)行了
condition.await();的后續(xù)代碼,這里是因為此處只有一個等待線程。喚醒等待隊列的時間基本可以忽略。
在使用wait/notify實現(xiàn)等待通知截止的時候我們知道必須執(zhí)行完notify()方法所在的synchronized代碼塊之后才釋放鎖。在這里也差不多,必須執(zhí)行完sinal所在的try語句塊之后才釋放鎖,condition.await()后的語句才能被執(zhí)行。
注意:必須在condition.await()方法調(diào)用之前調(diào)用lock.lock()代碼獲得同步監(jiān)控器,不然會報錯。
重入鎖
重入鎖的意思是:線程可以重復(fù)獲取同一把鎖(鎖的對象相同)
重進(jìn)入意味著所有的請求是基于"每線程",而不是基于"每調(diào)用"的,重進(jìn)入的實現(xiàn)是通過為每個鎖關(guān)聯(lián)一個請求計數(shù)和一個占有它的線程,當(dāng)計數(shù)為0時,認(rèn)為鎖是未被占用的,線程請求一個未被占用的鎖時,JVM
將記錄鎖的占用這,并且將請求計數(shù)置為1,如果同一個線程再次請求這個鎖,計數(shù)將遞增,每次占用線程退出同步塊,計數(shù)器值將遞減,直到計數(shù)器達(dá)到0時,鎖被釋放。
如下程序所示:
public class ReentryLock {
final Lock lock = new ReentrantLock();
int value;
public int getValue() {
lock.lock();
try {
return value;
} finally {
lock.unlock();
}
}
public void setValue() {
lock.lock();
try {
//在此處如果鎖不能重入,則會發(fā)生阻塞。如果可以重入則可以加鎖成功。
value = getValue() + 100; //1
} finally {
lock.unlock();
}
}
}
調(diào)用serValue()方法,在1處,需要調(diào)用getValue()方法獲取鎖,如果不能重入的話,這再此處會發(fā)生阻塞,如果可以重入的話則會加鎖成功。
公平鎖和非公平鎖
在使用ReentrantLock的時候,你會發(fā)現(xiàn)ReentrantLock
這個類有兩個構(gòu)造函數(shù),一個是傳入fair參數(shù)的參數(shù),fair參數(shù)代表的是鎖的公平策略,如果傳入true就表示需要構(gòu)造一個公平鎖,反之則表示要構(gòu)造一個非公平鎖。公平鎖表示線程獲取鎖的順序是按照線程加鎖的順序來分配的,即先來先得的FIFO先進(jìn)先出順序,而非公平鎖就是一種獲取鎖的搶占機制,是隨機獲取鎖的,和公平鎖不一樣的就是先來的不一定先的到鎖,這樣可能造成某些線程一直拿不到鎖,結(jié)果也就是不公平的了。
公平鎖的示例代碼如下:
public class FairLock {
final Lock lock = new ReentrantLock(true);
//創(chuàng)建十個線程
public static void main(String[] args) {
final FairLock fairLock = new FairLock();
Runnable runnable = new Runnable() {
public void run() {
System.out.println("★線程" + Thread.currentThread().getName()
+ "運行了");
fairLock.setup();
}
};
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(runnable);
}
for (int i=0;i<10;i++) {
threads[i].start();
}
}
//執(zhí)行方法
public void setup() {
lock.lock();
try {
System.out.println("********" + Thread.currentThread().getName() + "獲得了鎖定");
} finally {
lock.unlock();
}
}
}
運行結(jié)果:
如上,構(gòu)建公平鎖之后,線程的執(zhí)行順序跟其加入的順序相同,如果我們將其改成非公平鎖的話。
final Lock lock = new ReentrantLock(false);
運行結(jié)果如下:
則執(zhí)行順序不一定是按照其加入的順序來的,會出現(xiàn)搶占。
總結(jié)
本文主要介紹了lock的相關(guān)知識,介紹了其余synchronized相比較的不同點。lock 作為并發(fā)包中的一員,使用要比synchronized要更加靈活,然后,介紹了Conditon對象,重入鎖以及公平鎖和非公平鎖。
本文首發(fā)于:
https://mp.weixin.qq.com/s/kat0u5qQE4u_-eyjG9smKg
參考
https://time.geekbang.org/column/article/88487
https://juejin.im/post/5ab9a5b46fb9a028ce7b9b7e
《Java并發(fā)編程實踐》
作者:碼農(nóng)飛哥
微信公眾號:碼農(nóng)飛哥
