來源:SpringChang 發(fā)布時間:2018-11-14 10:53:39 閱讀量:1361
關(guān)于線程安全的例子,我前面的文章Java并發(fā)編程:線程安全和ThreadLocal里面提到了,簡而言之就是多個線程在同時訪問或修改公共資源的時候,由于不同線程搶占公共資源而導致的結(jié)果不確定性,就是在并發(fā)編程中經(jīng)常要考慮的線程安全問題。前面的做法是使用同步語句synchronized來隱式加鎖,現(xiàn)在我們嘗試來用Lock顯式加鎖來解決線程安全的問題,先來看一下Lock接口的定義:
public interface Lock
1
Lock接口有幾個重要的方法:
//獲取鎖,如果鎖不可用,出于線程調(diào)度目的,將禁用當前線程,并且在獲得鎖之前,該線程將一直處于休眠狀態(tài)。
void lock()
//釋放鎖,
void unlock()
1
2
3
4
lock()和unlock()是Lock接口的兩個重要方法,下面的案例將會使用到它倆。Lock是一個接口,實現(xiàn)它的子類包括:可重入鎖:ReentrantLock, 讀寫鎖中的只讀鎖:ReentrantReadWriteLock.ReadLock和讀寫鎖中的只寫鎖:ReentrantReadWriteLock.WriteLock 。我們先來用一用ReentrantLock可重入鎖來解決線程安全問題,如何還不明白什么是線程安全的同學可以回頭看我文章開頭給的鏈接文章。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyThread implements Runnable {
private int number = 5; //公共變量,5個線程都會訪問和修改該變量
private Lock lock = new ReentrantLock(); //可重入鎖
@Override
public void run() {
lock.lock(); //進方法的第一件事就是鎖住該方法,不能讓其他線程進來
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個線程
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
控制臺輸出:
線程 : t1獲取到了公共資源,number = 4
線程 : t2獲取到了公共資源,number = 3
線程 : t3獲取到了公共資源,number = 2
線程 : t4獲取到了公共資源,number = 1
線程 : t5獲取到了公共資源,number = 0
1
2
3
4
5
程序中創(chuàng)建了一把鎖,一個公共變量的資源,和5個線程,每起一個線程就會對公共資源number做自減操作,從上面的輸出可以看到程序中的5個線程對number的操作得到正確的結(jié)果。需要注意的是,在你加鎖的代碼塊的finaly語句一定要釋放鎖,就是調(diào)用一下lock的unlock()方法。
現(xiàn)在來看一下什么是可重入鎖 ,可重入鎖就是同一個線程多次嘗試進入同步代碼塊的時候,能夠順利的進去并執(zhí)行。實例代碼如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyThread implements Runnable {
private int number = 5; //公共變量,5個線程都會訪問和修改該變量
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(); //進方法的第一件事就是鎖住該方法,不能讓其他線程進來
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個線程
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
上述代碼什么意思呢?意思是每起一個線程的時候,線程運行run方法的時候,需要去調(diào)用sayHello()方法,那個sayHello()也是一個需要同步的和保證安全的方法,方法的第一行代碼一來就給方法上鎖,然后做完自己的工作之后再釋放鎖,工作期間,禁止其他線程進來,除了本線程除外。上面代碼輸出:
線程 : 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
實現(xiàn)一把簡單的鎖
如果你明白了上面幾個例子是用來干嘛的,好,我們可以繼續(xù)進行下去了,我們來實現(xiàn)一把最簡單的鎖。先不考慮這把鎖的公平性和可重入性,只要求達到當使用這把鎖的時候我們的代碼快安全即可。
我們先來定義自己的一把鎖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
定義自己的鎖需要實現(xiàn)Lock接口,而上面是Lock接口需要實現(xiàn)的方法,我們拋開其他因素,只看lock()和unlock()方法。
public class MyLock implements Lock {
private boolean isLocked = false; //定義一個變量,標記鎖是否被使用
@Override
public synchronized void lock() {
while(isLocked) { //不斷的重復判斷,isLocked是否被使用,如果已經(jīng)被占用,則讓新進來想嘗試獲取鎖的線程等待,直到被正在運行的線程喚醒
try {
wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
//進入該代碼塊有兩種情況:
// 1.第一個線程進來,此時isLocked變量的值為false,線程沒有進入while循環(huán)體里面
// 2.線程進入那個循環(huán)體里面,調(diào)用了wait()方法并經(jīng)歷了等待階段,現(xiàn)在已經(jīng)被另一個線程喚醒,
// 喚醒它的線程將那個變量isLocked設置為true,該線程才跳出了while循環(huán)體
//跳出while循環(huán)體,本線程做的第一件事就是趕緊占用線程,并告訴其他線程說:嘿,哥們,我占用了,你必須等待
isLocked = true; //將isLocked變量設置為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() {
//線程釋放鎖,釋放鎖的過程分為兩步
//1. 將標志變量設置為true,告訴其他線程,你可以占用了,不必死循環(huán)了
//2. 喚醒正在等待中的線程,讓他們?nèi)娭瀑Y源
isLocked = false;
notifyAll(); //通知所有等待的線程,誰搶到我不管
}
@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
從上面代碼可以看到,這把鎖還是照樣用到了同步語句synchronized,只是同步的過程我們自己來實現(xiàn),用戶只需要調(diào)用我們的鎖上鎖和釋放鎖就行了。其核心思想是用一個公共變量isLocked來標志當前鎖是否被占用,如果被占用則當前線程等待,然后每被喚醒一次就嘗試去搶那把鎖一次(處于等待狀態(tài)的線程不止當前線程一個),這是lock方法里面使用那個while循環(huán)的原因。當線程釋放鎖時,首先將isLocked變量置為false,表示鎖沒有被占用,其實線程可以使用了,并調(diào)用notifyAll()方法喚醒正在等待的線程,至于誰搶到我不管,不是本寶寶份內(nèi)的事。
那么上面我們實現(xiàn)的鎖是不是一把可重入的鎖呢?我們來調(diào)用sayHello()方法看看:
import java.util.concurrent.locks.Lock;
public class MyThread implements Runnable {
private int number = 5; //公共變量,5個線程都會訪問和修改該變量
private Lock lock = new MyLock(); //創(chuàng)建一把自己的鎖
public void sayHello(String threadName) {
System.out.println(Thread.currentThread().getName() + "線程進來,需要占用鎖");
lock.lock();
System.out.println("Hello!線程: " + threadName);
lock.unlock();
}
@Override
public void run() {
lock.lock(); //進方法的第一件事就是鎖住該方法,不能讓其他線程進來
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個線程
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方法加鎖之前打印一下當前線程的名稱,現(xiàn)在控制臺輸出如下:
線程 : t1獲取到了公共資源,number = 4
t1線程進來,需要占用鎖
1
2
如上所述,t1線程啟動并對公共變量做自減的時候,調(diào)用了sayHello方法。同一個線程t1,在線程啟動的時候獲得過一次鎖,再在調(diào)用sayHello也想要獲取這把鎖,這樣的需求我們是可以理解的,畢竟sayHello方法也時候也需要達到線程安全效果嘛??蓡栴}是痛一個線程嘗試獲取鎖兩次,程序就被卡住了,t1在run方法的時候獲得過鎖,在sayHello方法想再次獲得鎖的時候被告訴說:唉,哥們,該鎖被使用了,至于誰在使用我不管(雖然正在使用該鎖線程就是我自己),你還是等等吧!所以導致結(jié)果就是sayHello處于等待狀態(tài),而run方法則等待sayHello執(zhí)行完??刂婆_則一直處于運行狀態(tài)。
如果你不理解什么是可重入鎖和不可重入鎖,對比一下上面使用MyLock的例子和使用J.U.C.包下的ReentrantLock倆例子的區(qū)別,ReentrantLock是可重入的,而MyLock是不可重入的。
實現(xiàn)一把可重入鎖
現(xiàn)在我們來改裝一下這把鎖,讓他變成可重入鎖,也就是說:如果我已經(jīng)獲得了該鎖并且還沒釋放,我想再進來幾次都行。核心思路是:用一個線程標記變量記錄當前正在執(zhí)行的線程,如果當前想嘗試獲得鎖的線程等于正在執(zhí)行的線程,則獲取鎖成功。此外還需要用一個計數(shù)器來記錄一下本線程進來過多少次,因為如果同步方法調(diào)用unlock()時,我不一定就要釋放鎖,只有本線程的所有加鎖方法都釋放鎖的時候我才真正的釋放鎖,計數(shù)器就起到這個功能。
改裝過后的代碼如下:
public class MyLock implements Lock {
private boolean isLocked = false; //定義一個變量,標記鎖是否被使用
private Thread runningThread = null; //第一次線程進來的時候,正在運行的線程為null
private int count = 0; //計數(shù)器
@Override
public synchronized void lock() {
Thread currentThread = Thread.currentThread();
//不斷的重復判斷,isLocked是否被使用,如果已經(jīng)被占用,則讓新進來想嘗試獲取鎖的線程等待,直到被正在運行的線程喚醒
//除了判斷當前鎖是否被占用之外,還要判斷正在占用該鎖的是不是本線程自己
while(isLocked && currentThread != runningThread) { //如果鎖已經(jīng)被占用,而占用者又是自己,則不進入while循環(huán)
try {
wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
//進入該代碼塊有三種情況:
// 1.第一個線程進來,此時isLocked變量的值為false,線程沒有進入while循環(huán)體里面
// 2.線程進入那個循環(huán)體里面,調(diào)用了wait()方法并經(jīng)歷了等待階段,現(xiàn)在已經(jīng)被另一個線程喚醒,
// 3.線程不是第一次進來,但是新進來的線程就是正在運行的線程,則直接來到這個代碼塊
// 喚醒它的線程將那個變量isLocked設置為true,該線程才跳出了while循環(huán)體
//跳出while循環(huán)體,本線程做的第一件事就是趕緊占用線程,并告訴其他線程說:嘿,哥們,我占用了,你必須等待,計數(shù)器+1,并設置runningThread的值
isLocked = true; //將isLocked變量設置為true,表示本線程已經(jīng)占用
runningThread = currentThread; //給正在運行的線程變量賦值
count++; //計數(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() {
//線程釋放鎖,釋放鎖的過程分為三步
//1. 判斷發(fā)出釋放鎖的請求是否是當前線程
//2. 判斷計數(shù)器是否歸零,也就是說,判斷本線程自己進來了多少次,是不是全釋放鎖了
//3. 還原標志變量
if(runningThread == Thread.currentThread()) {
count--;//計數(shù)器自減
if(count == 0) { //判斷是否歸零
isLocked = false; //將鎖的狀態(tài)標志為未占用
runningThread = null; //既然已經(jīng)真正釋放了鎖,正在運行的線程則為null
notifyAll(); //通知所有等待的線程,誰搶到我不管
}
}
}
@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
如代碼注釋所述,這里新增了兩個變量runningThread和count,用于記錄當前正在執(zhí)行的線程和當前線程獲得鎖的次數(shù)。代碼的關(guān)鍵點在于while循環(huán)判斷測試獲得鎖的線程的條件,之前是只要鎖被占用就讓進來的線程等待,現(xiàn)在的做法是,如果鎖已經(jīng)被占用,則判斷一下正在占用這把鎖的就是我自己,如果是,則獲得鎖,計數(shù)器+1;如果不是,則新進來的線程進入等待。相應的,當線程調(diào)用unlock()釋放鎖的時候,并不是立馬就釋放該鎖,而是判斷當前線程還有沒有其他方法還在占用鎖,如果有,除了讓計數(shù)器減1之外什么事都別干,讓最后一個釋放鎖的方法來做最后的清除工作,當計數(shù)器歸零時,才表示真正的釋放鎖。
我知道你在懷疑這把被改造過后的鎖是不是能滿足我們的需求,現(xiàn)在就讓我們來運行一下程序,控制臺輸出如下:
線程 : t1獲取到了公共資源,number = 4
t1線程進來,需要占用鎖
Hello!線程: t1
線程 : t5獲取到了公共資源,number = 3
t5線程進來,需要占用鎖
Hello!線程: t5
線程 : t2獲取到了公共資源,number = 2
t2線程進來,需要占用鎖
Hello!線程: t2
線程 : t4獲取到了公共資源,number = 1
t4線程進來,需要占用鎖
Hello!線程: t4
線程 : t3獲取到了公共資源,number = 0
t3線程進來,需要占用鎖
Hello!線程: t3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
嗯,沒錯,這就是我們想要的結(jié)果。
好了,自己動手寫一把可重入鎖就先寫到這了,后面有時間再寫一篇用AQS實現(xiàn)的可重入鎖,畢竟ReentrantLock這哥們就是用AQS實現(xiàn)的可重入鎖,至于什么是AQS以及如何用AQS實現(xiàn)一把可重入鎖,且聽我慢慢道來。如果你看懂這篇文章的思路或者如果是你看完了這篇文章有動手寫一把可重入鎖的沖動,麻煩點個贊哦,畢竟大半夜的寫文章挺累的,是吧?
---------------------