來(lái)源:SpringChang 發(fā)布時(shí)間:2018-11-14 10:53:39 閱讀量:1112
關(guān)于線程安全的例子,我前面的文章Java并發(fā)編程:線程安全和ThreadLocal里面提到了,簡(jiǎn)而言之就是多個(gè)線程在同時(shí)訪問(wèn)或修改公共資源的時(shí)候,由于不同線程搶占公共資源而導(dǎo)致的結(jié)果不確定性,就是在并發(fā)編程中經(jīng)常要考慮的線程安全問(wèn)題。前面的做法是使用同步語(yǔ)句synchronized來(lái)隱式加鎖,現(xiàn)在我們嘗試來(lái)用Lock顯式加鎖來(lái)解決線程安全的問(wèn)題,先來(lái)看一下Lock接口的定義:
public interface Lock
1
Lock接口有幾個(gè)重要的方法:
//獲取鎖,如果鎖不可用,出于線程調(diào)度目的,將禁用當(dāng)前線程,并且在獲得鎖之前,該線程將一直處于休眠狀態(tài)。
void lock()
//釋放鎖,
void unlock()
1
2
3
4
lock()和unlock()是Lock接口的兩個(gè)重要方法,下面的案例將會(huì)使用到它倆。Lock是一個(gè)接口,實(shí)現(xiàn)它的子類包括:可重入鎖:ReentrantLock, 讀寫鎖中的只讀鎖:ReentrantReadWriteLock.ReadLock和讀寫鎖中的只寫鎖:ReentrantReadWriteLock.WriteLock 。我們先來(lái)用一用ReentrantLock可重入鎖來(lái)解決線程安全問(wèn)題,如何還不明白什么是線程安全的同學(xué)可以回頭看我文章開頭給的鏈接文章。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyThread implements Runnable {
private int number = 5; //公共變量,5個(gè)線程都會(huì)訪問(wèn)和修改該變量
private Lock lock = new ReentrantLock(); //可重入鎖
@Override
public void run() {
lock.lock(); //進(jìn)方法的第一件事就是鎖住該方法,不能讓其他線程進(jìn)來(lái)
try {
number--;
System.out.println("線程 : " + Thread.currentThread().getName() + "獲取到了公共資源,number = " + number);
Thread.sleep((long)(Math.random()*1000));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); //釋放鎖
}
}
public static void main(String[] args) {
//起5個(gè)線程
MyThread mt = new MyThread();
Thread t1 = new Thread(mt, "t1");
Thread t2 = new Thread(mt, "t2");
Thread t3 = new Thread(mt, "t3");
Thread t4 = new Thread(mt, "t4");
Thread t5 = new Thread(mt, "t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
控制臺(tái)輸出:
線程 : t1獲取到了公共資源,number = 4
線程 : t2獲取到了公共資源,number = 3
線程 : t3獲取到了公共資源,number = 2
線程 : t4獲取到了公共資源,number = 1
線程 : t5獲取到了公共資源,number = 0
1
2
3
4
5
程序中創(chuàng)建了一把鎖,一個(gè)公共變量的資源,和5個(gè)線程,每起一個(gè)線程就會(huì)對(duì)公共資源number做自減操作,從上面的輸出可以看到程序中的5個(gè)線程對(duì)number的操作得到正確的結(jié)果。需要注意的是,在你加鎖的代碼塊的finaly語(yǔ)句一定要釋放鎖,就是調(diào)用一下lock的unlock()方法。
現(xiàn)在來(lái)看一下什么是可重入鎖 ,可重入鎖就是同一個(gè)線程多次嘗試進(jìn)入同步代碼塊的時(shí)候,能夠順利的進(jìn)去并執(zhí)行。實(shí)例代碼如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyThread implements Runnable {
private int number = 5; //公共變量,5個(gè)線程都會(huì)訪問(wèn)和修改該變量
private Lock lock = new ReentrantLock(); //可重入鎖
public void sayHello(String threadName) {
lock.lock();
System.out.println("Hello!線程: " + threadName);
lock.unlock();
}
@Override
public void run() {
lock.lock(); //進(jìn)方法的第一件事就是鎖住該方法,不能讓其他線程進(jìn)來(lái)
try {
number--;
System.out.println("線程 : " + Thread.currentThread().getName() + "獲取到了公共資源,number = " + number);
Thread.sleep((long)(Math.random()*1000));
sayHello(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); //釋放鎖
}
}
public static void main(String[] args) {
//起5個(gè)線程
MyThread mt = new MyThread();
Thread t1 = new Thread(mt, "t1");
Thread t2 = new Thread(mt, "t2");
Thread t3 = new Thread(mt, "t3");
Thread t4 = new Thread(mt, "t4");
Thread t5 = new Thread(mt, "t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
上述代碼什么意思呢?意思是每起一個(gè)線程的時(shí)候,線程運(yùn)行run方法的時(shí)候,需要去調(diào)用sayHello()方法,那個(gè)sayHello()也是一個(gè)需要同步的和保證安全的方法,方法的第一行代碼一來(lái)就給方法上鎖,然后做完自己的工作之后再釋放鎖,工作期間,禁止其他線程進(jìn)來(lái),除了本線程除外。上面代碼輸出:
線程 : t1獲取到了公共資源,number = 4
Hello!線程: t1
線程 : t2獲取到了公共資源,number = 3
Hello!線程: t2
線程 : t3獲取到了公共資源,number = 2
Hello!線程: t3
線程 : t4獲取到了公共資源,number = 1
Hello!線程: t4
線程 : t5獲取到了公共資源,number = 0
Hello!線程: t5
1
2
3
4
5
6
7
8
9
10
實(shí)現(xiàn)一把簡(jiǎn)單的鎖
如果你明白了上面幾個(gè)例子是用來(lái)干嘛的,好,我們可以繼續(xù)進(jìn)行下去了,我們來(lái)實(shí)現(xiàn)一把最簡(jiǎn)單的鎖。先不考慮這把鎖的公平性和可重入性,只要求達(dá)到當(dāng)使用這把鎖的時(shí)候我們的代碼快安全即可。
我們先來(lái)定義自己的一把鎖MyLock。
public class MyLock implements Lock {
@Override
public void lock() {
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
}
@Override
public Condition newCondition() {
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
定義自己的鎖需要實(shí)現(xiàn)Lock接口,而上面是Lock接口需要實(shí)現(xiàn)的方法,我們拋開其他因素,只看lock()和unlock()方法。
public class MyLock implements Lock {
private boolean isLocked = false; //定義一個(gè)變量,標(biāo)記鎖是否被使用
@Override
public synchronized void lock() {
while(isLocked) { //不斷的重復(fù)判斷,isLocked是否被使用,如果已經(jīng)被占用,則讓新進(jìn)來(lái)想嘗試獲取鎖的線程等待,直到被正在運(yùn)行的線程喚醒
try {
wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
//進(jìn)入該代碼塊有兩種情況:
// 1.第一個(gè)線程進(jìn)來(lái),此時(shí)isLocked變量的值為false,線程沒有進(jìn)入while循環(huán)體里面
// 2.線程進(jìn)入那個(gè)循環(huán)體里面,調(diào)用了wait()方法并經(jīng)歷了等待階段,現(xiàn)在已經(jīng)被另一個(gè)線程喚醒,
// 喚醒它的線程將那個(gè)變量isLocked設(shè)置為true,該線程才跳出了while循環(huán)體
//跳出while循環(huán)體,本線程做的第一件事就是趕緊占用線程,并告訴其他線程說(shuō):嘿,哥們,我占用了,你必須等待
isLocked = true; //將isLocked變量設(shè)置為true,表示本線程已經(jīng)占用
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public synchronized void unlock() {
//線程釋放鎖,釋放鎖的過(guò)程分為兩步
//1. 將標(biāo)志變量設(shè)置為true,告訴其他線程,你可以占用了,不必死循環(huán)了
//2. 喚醒正在等待中的線程,讓他們?nèi)?qiáng)制資源
isLocked = false;
notifyAll(); //通知所有等待的線程,誰(shuí)搶到我不管
}
@Override
public Condition newCondition() {
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
從上面代碼可以看到,這把鎖還是照樣用到了同步語(yǔ)句synchronized,只是同步的過(guò)程我們自己來(lái)實(shí)現(xiàn),用戶只需要調(diào)用我們的鎖上鎖和釋放鎖就行了。其核心思想是用一個(gè)公共變量isLocked來(lái)標(biāo)志當(dāng)前鎖是否被占用,如果被占用則當(dāng)前線程等待,然后每被喚醒一次就嘗試去搶那把鎖一次(處于等待狀態(tài)的線程不止當(dāng)前線程一個(gè)),這是lock方法里面使用那個(gè)while循環(huán)的原因。當(dāng)線程釋放鎖時(shí),首先將isLocked變量置為false,表示鎖沒有被占用,其實(shí)線程可以使用了,并調(diào)用notifyAll()方法喚醒正在等待的線程,至于誰(shuí)搶到我不管,不是本寶寶份內(nèi)的事。
那么上面我們實(shí)現(xiàn)的鎖是不是一把可重入的鎖呢?我們來(lái)調(diào)用sayHello()方法看看:
import java.util.concurrent.locks.Lock;
public class MyThread implements Runnable {
private int number = 5; //公共變量,5個(gè)線程都會(huì)訪問(wèn)和修改該變量
private Lock lock = new MyLock(); //創(chuàng)建一把自己的鎖
public void sayHello(String threadName) {
System.out.println(Thread.currentThread().getName() + "線程進(jìn)來(lái),需要占用鎖");
lock.lock();
System.out.println("Hello!線程: " + threadName);
lock.unlock();
}
@Override
public void run() {
lock.lock(); //進(jìn)方法的第一件事就是鎖住該方法,不能讓其他線程進(jìn)來(lái)
try {
number--;
System.out.println("線程 : " + Thread.currentThread().getName() + "獲取到了公共資源,number = " + number);
Thread.sleep((long)(Math.random()*1000));
sayHello(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); //釋放鎖
}
}
public static void main(String[] args) {
//起5個(gè)線程
MyThread mt = new MyThread();
Thread t1 = new Thread(mt, "t1");
Thread t2 = new Thread(mt, "t2");
Thread t3 = new Thread(mt, "t3");
Thread t4 = new Thread(mt, "t4");
Thread t5 = new Thread(mt, "t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
為了特意演示效果,我在sayHello方法加鎖之前打印一下當(dāng)前線程的名稱,現(xiàn)在控制臺(tái)輸出如下:
線程 : t1獲取到了公共資源,number = 4
t1線程進(jìn)來(lái),需要占用鎖
1
2
如上所述,t1線程啟動(dòng)并對(duì)公共變量做自減的時(shí)候,調(diào)用了sayHello方法。同一個(gè)線程t1,在線程啟動(dòng)的時(shí)候獲得過(guò)一次鎖,再在調(diào)用sayHello也想要獲取這把鎖,這樣的需求我們是可以理解的,畢竟sayHello方法也時(shí)候也需要達(dá)到線程安全效果嘛??蓡?wèn)題是痛一個(gè)線程嘗試獲取鎖兩次,程序就被卡住了,t1在run方法的時(shí)候獲得過(guò)鎖,在sayHello方法想再次獲得鎖的時(shí)候被告訴說(shuō):唉,哥們,該鎖被使用了,至于誰(shuí)在使用我不管(雖然正在使用該鎖線程就是我自己),你還是等等吧!所以導(dǎo)致結(jié)果就是sayHello處于等待狀態(tài),而run方法則等待sayHello執(zhí)行完??刂婆_(tái)則一直處于運(yùn)行狀態(tài)。
如果你不理解什么是可重入鎖和不可重入鎖,對(duì)比一下上面使用MyLock的例子和使用J.U.C.包下的ReentrantLock倆例子的區(qū)別,ReentrantLock是可重入的,而MyLock是不可重入的。
實(shí)現(xiàn)一把可重入鎖
現(xiàn)在我們來(lái)改裝一下這把鎖,讓他變成可重入鎖,也就是說(shuō):如果我已經(jīng)獲得了該鎖并且還沒釋放,我想再進(jìn)來(lái)幾次都行。核心思路是:用一個(gè)線程標(biāo)記變量記錄當(dāng)前正在執(zhí)行的線程,如果當(dāng)前想嘗試獲得鎖的線程等于正在執(zhí)行的線程,則獲取鎖成功。此外還需要用一個(gè)計(jì)數(shù)器來(lái)記錄一下本線程進(jìn)來(lái)過(guò)多少次,因?yàn)槿绻椒椒ㄕ{(diào)用unlock()時(shí),我不一定就要釋放鎖,只有本線程的所有加鎖方法都釋放鎖的時(shí)候我才真正的釋放鎖,計(jì)數(shù)器就起到這個(gè)功能。
改裝過(guò)后的代碼如下:
public class MyLock implements Lock {
private boolean isLocked = false; //定義一個(gè)變量,標(biāo)記鎖是否被使用
private Thread runningThread = null; //第一次線程進(jìn)來(lái)的時(shí)候,正在運(yùn)行的線程為null
private int count = 0; //計(jì)數(shù)器
@Override
public synchronized void lock() {
Thread currentThread = Thread.currentThread();
//不斷的重復(fù)判斷,isLocked是否被使用,如果已經(jīng)被占用,則讓新進(jìn)來(lái)想嘗試獲取鎖的線程等待,直到被正在運(yùn)行的線程喚醒
//除了判斷當(dāng)前鎖是否被占用之外,還要判斷正在占用該鎖的是不是本線程自己
while(isLocked && currentThread != runningThread) { //如果鎖已經(jīng)被占用,而占用者又是自己,則不進(jìn)入while循環(huán)
try {
wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
//進(jìn)入該代碼塊有三種情況:
// 1.第一個(gè)線程進(jìn)來(lái),此時(shí)isLocked變量的值為false,線程沒有進(jìn)入while循環(huán)體里面
// 2.線程進(jìn)入那個(gè)循環(huán)體里面,調(diào)用了wait()方法并經(jīng)歷了等待階段,現(xiàn)在已經(jīng)被另一個(gè)線程喚醒,
// 3.線程不是第一次進(jìn)來(lái),但是新進(jìn)來(lái)的線程就是正在運(yùn)行的線程,則直接來(lái)到這個(gè)代碼塊
// 喚醒它的線程將那個(gè)變量isLocked設(shè)置為true,該線程才跳出了while循環(huán)體
//跳出while循環(huán)體,本線程做的第一件事就是趕緊占用線程,并告訴其他線程說(shuō):嘿,哥們,我占用了,你必須等待,計(jì)數(shù)器+1,并設(shè)置runningThread的值
isLocked = true; //將isLocked變量設(shè)置為true,表示本線程已經(jīng)占用
runningThread = currentThread; //給正在運(yùn)行的線程變量賦值
count++; //計(jì)數(shù)器自增
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public synchronized void unlock() {
//線程釋放鎖,釋放鎖的過(guò)程分為三步
//1. 判斷發(fā)出釋放鎖的請(qǐng)求是否是當(dāng)前線程
//2. 判斷計(jì)數(shù)器是否歸零,也就是說(shuō),判斷本線程自己進(jìn)來(lái)了多少次,是不是全釋放鎖了
//3. 還原標(biāo)志變量
if(runningThread == Thread.currentThread()) {
count--;//計(jì)數(shù)器自減
if(count == 0) { //判斷是否歸零
isLocked = false; //將鎖的狀態(tài)標(biāo)志為未占用
runningThread = null; //既然已經(jīng)真正釋放了鎖,正在運(yùn)行的線程則為null
notifyAll(); //通知所有等待的線程,誰(shuí)搶到我不管
}
}
}
@Override
public Condition newCondition() {
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
如代碼注釋所述,這里新增了兩個(gè)變量runningThread和count,用于記錄當(dāng)前正在執(zhí)行的線程和當(dāng)前線程獲得鎖的次數(shù)。代碼的關(guān)鍵點(diǎn)在于while循環(huán)判斷測(cè)試獲得鎖的線程的條件,之前是只要鎖被占用就讓進(jìn)來(lái)的線程等待,現(xiàn)在的做法是,如果鎖已經(jīng)被占用,則判斷一下正在占用這把鎖的就是我自己,如果是,則獲得鎖,計(jì)數(shù)器+1;如果不是,則新進(jìn)來(lái)的線程進(jìn)入等待。相應(yīng)的,當(dāng)線程調(diào)用unlock()釋放鎖的時(shí)候,并不是立馬就釋放該鎖,而是判斷當(dāng)前線程還有沒有其他方法還在占用鎖,如果有,除了讓計(jì)數(shù)器減1之外什么事都別干,讓最后一個(gè)釋放鎖的方法來(lái)做最后的清除工作,當(dāng)計(jì)數(shù)器歸零時(shí),才表示真正的釋放鎖。
我知道你在懷疑這把被改造過(guò)后的鎖是不是能滿足我們的需求,現(xiàn)在就讓我們來(lái)運(yùn)行一下程序,控制臺(tái)輸出如下:
線程 : t1獲取到了公共資源,number = 4
t1線程進(jìn)來(lái),需要占用鎖
Hello!線程: t1
線程 : t5獲取到了公共資源,number = 3
t5線程進(jìn)來(lái),需要占用鎖
Hello!線程: t5
線程 : t2獲取到了公共資源,number = 2
t2線程進(jìn)來(lái),需要占用鎖
Hello!線程: t2
線程 : t4獲取到了公共資源,number = 1
t4線程進(jìn)來(lái),需要占用鎖
Hello!線程: t4
線程 : t3獲取到了公共資源,number = 0
t3線程進(jìn)來(lái),需要占用鎖
Hello!線程: t3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
嗯,沒錯(cuò),這就是我們想要的結(jié)果。
好了,自己動(dòng)手寫一把可重入鎖就先寫到這了,后面有時(shí)間再寫一篇用AQS實(shí)現(xiàn)的可重入鎖,畢竟ReentrantLock這哥們就是用AQS實(shí)現(xiàn)的可重入鎖,至于什么是AQS以及如何用AQS實(shí)現(xiàn)一把可重入鎖,且聽我慢慢道來(lái)。如果你看懂這篇文章的思路或者如果是你看完了這篇文章有動(dòng)手寫一把可重入鎖的沖動(dòng),麻煩點(diǎn)個(gè)贊哦,畢竟大半夜的寫文章挺累的,是吧?
---------------------
在線
客服
服務(wù)時(shí)間:周一至周日 08:30-18:00
選擇下列產(chǎn)品馬上在線溝通:
客服
熱線
7*24小時(shí)客服服務(wù)熱線
關(guān)注
微信
關(guān)注官方微信