多線程在提高效率的同時,必然面臨線程安全的問題,Java中提供了一些機制來解決線程安全問題。
當多個線程同時訪問臨界資源(或叫共享資源)(一個對象,對象中的屬性,一個文件,一個數據庫等)時,就可能會產生線程安全問題。
不過,當多個線程執行一個方法,方法內部的局部變量並不是臨界資源,因為方法是在棧上執行的,而Java棧是線程私有的,因此不會產生線程安全問題。
解決方案:序列化訪問臨界資源”的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱作同步互斥訪問。
在Java中,提供了兩種方式來實現同步互斥訪問:synchronized和Lock。
1.synchronized
(1)synchronized方法
例子:兩個線程分別調用insertData對象插入數據:
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
|
public
class
Test {
public
static
void
main(String[] args) {
final
InsertData insertData =
new
InsertData();
new
Thread() {
public
void
run() {
insertData.insert(Thread.currentThread());
};
}.start();
new
Thread() {
public
void
run() {
insertData.insert(Thread.currentThread());
};
}.start();
}
}
class
InsertData {
private
ArrayList<integer> arrayList =
new
ArrayList<integer>();
public
void
insert(Thread thread){
for
(
int
i=
0
;i<
5
;i++){
System.out.println(thread.getName()+
"在插入數據"
+i);
arrayList.add(i);
}
}
}</integer></integer>
|
此時程序的輸出結果為:
說明兩個線程在同時執行insert方法。
而如果在insert方法前面加上關鍵字synchronized的話,運行結果為:
1
2
3
4
5
6
7
8
9
10
|
class
InsertData {
private
ArrayList<integer> arrayList =
new
ArrayList<integer>();
public
synchronized
void
insert(Thread thread){
for
(
int
i=
0
;i<
5
;i++){
System.out.println(thread.getName()+
"在插入數據"
+i);
arrayList.add(i);
}
}
}</integer></integer>
|
從上輸出結果說明,Thread-1插入數據是等Thread-0插入完數據之后才進行的。說明Thread-0和Thread-1是順序執行insert方法的。
這就是synchronized方法。
注意:
1)當一個線程正在訪問一個對象的synchronized方法,那么其他線程不能訪問該對象的其他synchronized方法。這個原因很簡單,因為一個對象只有一把鎖,當一個線程獲取了該對象的鎖之后,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized方法。
2)當一個線程正在訪問一個對象的synchronized方法,那么其他線程能訪問該對象的非synchronized方法。這個原因很簡單,訪問非synchronized方法不需要獲得該對象的鎖,假如一個方法沒用synchronized關鍵字修飾,說明它不會使用到臨界資源,那么其他線程是可以訪問這個方法的,
3)如果一個線程A需要訪問對象object1的synchronized方法fun1,另外一個線程B需要訪問對象object2的synchronized方法fun1,即使object1和object2是同一類型),也不會產生線程安全問題,因為他們訪問的是不同的對象,所以不存在互斥問題。
(2)synchronized代碼塊
synchronized代碼塊類似於以下這種形式:
synchronized(synObject) { }
當在某個線程中執行這段代碼塊,該線程會獲取對象synObject的鎖,從而使得其他線程無法同時訪問該代碼塊。
synObject可以是this,代表獲取當前對象的鎖,也可以是類中的一個屬性,代表獲取該屬性的鎖。
比如上面的insert方法可以改成以下兩種形式:
1
2
3
4
5
6
7
8
9
10
11
12
|
<span style=
"font-size:14px;"
>
class
InsertData {
private
ArrayList<integer> arrayList =
new
ArrayList<integer>();
public
void
insert(Thread thread){
synchronized
(
this
) {
for
(
int
i=
0
;i<
100
;i++){
System.out.println(thread.getName()+
"在插入數據"
+i);
arrayList.add(i);
}
}
}
}</integer></integer></span>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<span style=
"font-size:14px;"
>
class
InsertData {
private
ArrayList<integer> arrayList =
new
ArrayList<integer>();
private
Object object =
new
Object();
public
void
insert(Thread thread){
synchronized
(object) {
for
(
int
i=
0
;i<
100
;i++){
System.out.println(thread.getName()+
"在插入數據"
+i);
arrayList.add(i);
}
}
}
}</integer></integer></span>
|
從上面可以看出,synchronized代碼塊使用起來比synchronized方法要靈活得多。synchronized代碼塊可以實現只對需要同步的地方進行同步。
另外,每個類也會有一個鎖,它可以用來控制對static數據成員的並發訪問。
並且如果一個線程執行一個對象的非static synchronized方法,另外一個線程需要執行這個對象所屬類的static synchronized方法,此時不會發生互斥現象,因為訪問static synchronized方法占用的是類鎖,而訪問非static synchronized方法占用的是對象鎖,所以不存在互斥現象。
看下面這段代碼就明白了:
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
|
public
class
Test {
public
static
void
main(String[] args) {
final
InsertData insertData =
new
InsertData();
new
Thread(){
@Override
public
void
run() {
insertData.insert();
}
}.start();
new
Thread(){
@Override
public
void
run() {
insertData.insert1();
}
}.start();
}
}
class
InsertData {
public
synchronized
void
insert(){
System.out.println(
"執行insert"
);
try
{
Thread.sleep(
5000
);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
System.out.println(
"執行insert完畢"
);
}
public
synchronized
static
void
insert1() {
System.out.println(
"執行insert1"
);
System.out.println(
"執行insert1完畢"
);
}
}
|
執行結果:
第一個線程里面執行的是insert方法,不會導致第二個線程執行insert1方法發生阻塞現象。
注意:對於synchronized方法或者synchronized代碼塊,當出現異常時,JVM會自動釋放當前線程占用的鎖,因此不會出現由於異常導致出現死鎖現象。
2.Lock
synchronized是java中的一個關鍵字,也就是說是Java語言內置的特性。
代碼塊被synchronized修飾了,當一個線程獲取了對應的鎖,並執行該代碼塊時,其他線程便只能一直等待,等待獲取鎖的線程釋放鎖,而這里獲取鎖的線程釋放鎖只會有兩種情況:
1)獲取鎖的線程執行完了該代碼塊,然后線程釋放對鎖的占有;
2)線程執行發生異常,此時JVM會讓線程自動釋放鎖。
如果這個獲取鎖的線程由於要等待IO或者其他原因(比如調用sleep方法)被阻塞了,但是又沒有釋放鎖,其他線程便只能等待,多么影響程序執行效率。
因此就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間或者能夠響應中斷),通過Lock就可以辦到。
再舉個例子:當有多個線程讀寫文件時,讀操作和寫操作會發生沖突現象,寫操作和寫操作會發生沖突現象,但是讀操作和讀操作不會發生沖突現象。
但是采用synchronized關鍵字來實現同步的話,就會導致一個問題:
如果多個線程都只是進行讀操作,所以當一個線程在進行讀操作時,其他線程只能等待無法進行讀操作。
因此就需要一種機制來使得多個線程都只是進行讀操作時,線程之間不會發生沖突,通過Lock就可以辦到。
另外,通過Lock可以知道線程有沒有成功獲取到鎖。這個是synchronized無法辦到的。
總結一下,也就是說Lock提供了比synchronized更多的功能。但是要注意以下幾點:
1)Lock不是Java語言內置的,synchronized是Java語言的關鍵字,因此是內置特性。Lock是一個類,通過這個類可以實現同步訪問;
2)Lock和synchronized有一點非常大的不同,采用synchronized不需要用戶去手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完之后,系統會自動讓線程釋放對鎖的占用;而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。
java.util.concurrent.locks包中常用的類和接口:
(1).Lock
Lock是一個接口:
1
2
3
4
5
6
7
8
|
public
interface
Lock {
void
lock();
void
lockInterruptibly()
throws
InterruptedException;
boolean
tryLock();
boolean
tryLock(
long
time, TimeUnit unit)
throws
InterruptedException;
void
unlock();
Condition newCondition();
}
|
lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的。unLock()方法是用來釋放鎖的。
在Lock中聲明了四個方法來獲取鎖,那么這四個方法有何區別呢?
<1>.lock():如果鎖已被其他線程獲取,則進行等待。采用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。
通常使用Lock來進行同步的話,是以下面這種形式去使用的:
Lock lock = …; lock.lock(); try{ //處理任務 }catch(Exception ex){ }finally{ lock.unlock();//釋放鎖 } <2>.tryLock():有返回值,表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。
tryLock(long time, TimeUnit unit): 和tryLock()方法是類似的,區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。
一般情況下通過tryLock來獲取鎖時是這樣使用的:
Lock lock = …; if(lock.tryLock()) { try{ //處理任務 }catch(Exception ex){ }finally{ lock.unlock();//釋放鎖 } }else{ //如果不能獲取鎖,則直接做其他事情 } <3>.lockInterruptibly()
獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態。當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那么對線程B調用thread B.interrupt()方法能夠中斷線程B的等待過程。
lockInterruptibly()一般的使用形式如下:
publicvoidmethod()throwsInterruptedException { lock.lockInterruptibly(); try{ //….. } finally{ lock.unlock(); } }
注意,當一個線程獲取了鎖之后,是不會被interrupt()方法中斷的。單獨調用interrupt()方法不能中斷正在運行過程中的線程,只能中斷阻塞過程中的線程。
因此當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的。
而用synchronized修飾的話,當一個線程處於等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。
(2). ReentrantLock
可重入鎖。ReentrantLock是唯一實現了Lock接口的類,並且ReentrantLock提供了更多的方法。
例子1,lock()的正確使用方法
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
|
public
class
Test {
private
ArrayList<integer> arrayList =
new
ArrayList<integer>();
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();
}
}
}</integer></integer>
|
輸出結果:
1
2
3
4
|
Thread-
0
得到了鎖
Thread-
1
得到了鎖
Thread-
0
釋放了鎖
Thread-
1
釋放了鎖
|
也許有朋友會問,怎么會輸出這個結果?第二個線程怎么會在第一個線程釋放鎖之前得到了鎖?原因在於,在insert方法中的lock變量是局部變量,每個線程執行該方法時都會保存一個副本,那么理所當然每個線程執行到lock.lock()處獲取的是不同的鎖,所以就不會發生沖突。
知道了原因改起來就比較容易了,只需要將lock聲明為類的屬性即可。
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
|
public
class
Test {
private
ArrayList<integer> arrayList =
new
ArrayList<integer>();
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();
}
}
}</integer></integer>
|
這樣就是正確地使用Lock的方法了。
例子2,tryLock()的使用方法
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
|
public
class
Test {
private
ArrayList<integer> arrayList =
new
ArrayList<integer>();
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()+
"獲取鎖失敗"
);
}
}
}</integer></integer>
|
輸出結果:
1
2
3
|
Thread-
0
得到了鎖
Thread-
1
獲取鎖失敗
Thread-
0
釋放了鎖
|
例子3,lockInterruptibly()響應中斷的使用方法:
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
|
public
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也是一個接口
publicinterfaceReadWriteLock { Lock readLock();//獲取讀鎖 Lock writeLock();//獲取寫鎖 }
將文件的讀寫操作分開,分成2個鎖來分配給線程,從而使得多個線程可以同時進行讀操作。
下面的ReentrantReadWriteLock實現了ReadWriteLock接口。
(4).ReentrantReadWriteLock
具體用法:有多個線程要同時進行讀操作
synchronized達到的效果:
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
|
public
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執行讀操作的信息。
ReentrantReadWriteLock達到的效果:
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
|
public
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();
}
}
}
|
結果是:thread1和thread2在同時進行讀操作。大大提升了讀操作的效率
如果有一個線程已經占用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。
如果有一個線程已經占用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。
Lock和synchronized的選擇:
總結來說,Lock和synchronized有以下幾點不同:
1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現;
2)synchronized在發生異常時,會自動釋放線程占有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
3)Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;
4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
5)Lock可以提高多個線程進行讀操作的效率。
在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當競爭資源非常激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優於synchronized。所以說,在具體使用時要根據適當情況選擇。
鎖的相關概念:
1.可重入鎖
如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性實際上表明了鎖的分配機制:基於線程的分配,而不是基於方法調用的分配。舉個簡單的例子,當一個線程執行到某個synchronized方法時,比如說method1,而在method1中會調用另外一個synchronized方法method2,此時線程不必重新去申請鎖,而是可以直接執行方法method2。
classMyClass { publicsynchronizedvoidmethod1() { method2(); } publicsynchronizedvoidmethod2() { } }
上述代碼中的兩個方法method1和method2都用synchronized修飾了,
假如synchronized不具備可重入性,某一時刻,線程A執行到了method1,此時線程A獲取了這個對象的鎖,而由於method2也是synchronized方法,此時線程A需要重新申請鎖。因為線程A已經持有了該對象的鎖,而又在申請獲取該對象的鎖,這樣就會線程A一直等待永遠不會獲取到鎖。
而由於synchronized和Lock都具備可重入性,所以不會發生上述現象。
2.可中斷鎖
在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。
如果某一線程A正在執行鎖中的代碼,另一線程B正在等待獲取該鎖,可能由於等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。
在前面演示lockInterruptibly()的用法時已經體現了Lock的可中斷性。
3.公平鎖
公平鎖即盡量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該所,這種就是公平鎖。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的。這樣就可能導致某個或者一些線程永遠獲取不到鎖。
在Java中,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。
而對於ReentrantLock和ReentrantReadWriteLock,它默認情況下是非公平鎖,但是可以設置為公平鎖。
ReentrantLock lock =newReentrantLock(true);//true表示為公平鎖,為fasle為非公平鎖。默認情況下,如果使用無參構造器,則是非公平鎖。
另外在ReentrantLock類中定義了很多方法,比如:
isFair() //判斷鎖是否是公平鎖
isLocked() //判斷鎖是否被任何線程獲取了
isHeldByCurrentThread() //判斷鎖是否被當前線程獲取了
hasQueuedThreads() //判斷是否有線程在等待該鎖
在ReentrantReadWriteLock中也有類似的方法,同樣也可以設置為公平鎖和非公平鎖。
不過要記住,ReentrantReadWriteLock並未實現Lock接口,它實現的是ReadWriteLock接口。
4.讀寫鎖
讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。
正因為有了讀寫鎖,才使得多個線程之間的讀操作不會發生沖突。
ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現了這個接口。
可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。