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