synchronized關(guān)鍵字
synchronized,我們謂之鎖,主要用來給方法、代碼塊加鎖。當(dāng)某個(gè)方法或者代碼塊使用synchronized時(shí),那么在同一時(shí)刻至多僅有有一個(gè)線程在執(zhí)行該段代碼。當(dāng)有多個(gè)線程訪問同一對(duì)象的加鎖方法/代碼塊時(shí),同一時(shí)間只有一個(gè)線程在執(zhí)行,其余線程必須要等待當(dāng)前線程執(zhí)行完之后才能執(zhí)行該代碼段。但是,其余線程是可以訪問該對(duì)象中的非加鎖代碼塊的。
synchronized主要包括兩種方法:synchronized 方法、synchronized 塊。
synchronized 方法
通過在方法聲明中加入 synchronized關(guān)鍵字來聲明 synchronized 方法。如:
public synchronized void getResult();
synchronized方法控制對(duì)類成員變量的訪問。它是如何來避免類成員變量的訪問控制呢?我們知道方法使用了synchronized關(guān)鍵字表明該方法已加鎖,在任一線程在訪問改方法時(shí)都必須要判斷該方法是否有其他線程在“獨(dú)占”。每個(gè)類實(shí)例對(duì)應(yīng)一個(gè)把鎖,每個(gè)synchronized方法都必須調(diào)用該方法的類實(shí)例的鎖方能執(zhí)行,否則所屬線程阻塞,方法一旦執(zhí)行,就獨(dú)占該鎖,直到從該方法返回時(shí)才將鎖釋放,被阻塞的線程方能獲得該鎖。
其實(shí)synchronized方法是存在缺陷的,如果我們將一個(gè)很大的方法聲明為synchronized將會(huì)大大影響效率的。如果多個(gè)線程在訪問一個(gè)synchronized方法,那么同一時(shí)刻只有一個(gè)線程在執(zhí)行該方法,而其他線程都必須等待,但是如果該方法沒有使用synchronized,則所有線程可以在同一時(shí)刻執(zhí)行它,減少了執(zhí)行的總時(shí)間。所以如果我們知道一個(gè)方法不會(huì)被多個(gè)線程執(zhí)行到或者說不存在資源共享的問題,則不需要使用synchronized關(guān)鍵字。但是如果一定要使用synchronized關(guān)鍵字,那么我們可以synchronized代碼塊來替換synchronized方法。
synchronized 塊
synchronized代碼塊所起到的作用和synchronized方法一樣,只不過它使臨界區(qū)變的盡可能短了,換句話說:它只把需要的共享數(shù)據(jù)保護(hù)起來,其余的長(zhǎng)代碼塊留出此操作。語法如下:
1
2
3
4
5
6
7
8
|
synchronized (object) { //允許訪問控制的代碼 } 如果我們需要以這種方式來使用 synchronized 關(guān)鍵字,那么必須要通過一個(gè)對(duì)象引用來作為參數(shù),通常這個(gè)參數(shù)我們常使用為 this . synchronized ( this ) { //允許訪問控制的代碼 } |
對(duì)于synchronized(this)有如下理解:
1、當(dāng)兩個(gè)并發(fā)線程訪問同一個(gè)對(duì)象object中的這個(gè)synchronized(this)同步代碼塊時(shí),一個(gè)時(shí)間內(nèi)只能有一個(gè)線程得到執(zhí)行。另一個(gè)線程必須等待當(dāng)前線程執(zhí)行完這個(gè)代碼塊以后才能執(zhí)行該代碼塊。
2、然而,當(dāng)一個(gè)線程訪問object的一個(gè)synchronized(this)同步代碼塊時(shí),另一個(gè)線程仍然可以訪問object中的非synchronized(this)同步代碼塊。
3、尤其關(guān)鍵的是,當(dāng)一個(gè)線程訪問object的一個(gè)synchronized(this)同步代碼塊時(shí),其他線程對(duì)object中所有其他synchronized(this)同步代碼塊得訪問將被阻塞。
4、第三個(gè)例子同樣適用其他同步代碼塊。也就是說,當(dāng)一個(gè)線程訪問object的一個(gè)synchronized(this)同步代碼塊時(shí),它就獲得了這個(gè)object的對(duì)象鎖。結(jié)果,其他線程對(duì)該object對(duì)象所有同步代碼部分的訪問都將被暫時(shí)阻塞。
鎖
在java多線程中存在一個(gè)“先來后到”的原則,也就是說誰先搶到鑰匙,誰先用。我們知道為避免資源競(jìng)爭(zhēng)產(chǎn)生問題,java使用同步機(jī)制來避免,而同步機(jī)制是使用鎖概念來控制的。那么在Java程序當(dāng)中,鎖是如何體現(xiàn)的呢?這里我們需要弄清楚兩個(gè)概念:
什么是鎖?在日常生活中,它就是一個(gè)加在門、箱子、抽屜等物體上的封緘器,防止別人偷窺或者偷盜,起到一個(gè)保護(hù)的作用。在java中同樣如此,鎖對(duì)對(duì)象起到一個(gè)保護(hù)的作用,一個(gè)線程如果獨(dú)占了某個(gè)資源,那么其他的線程別想用,想用?等我用完再說吧!
在java程序運(yùn)行環(huán)境中,JVM需要對(duì)兩類線程共享的數(shù)據(jù)進(jìn)行協(xié)調(diào):
1、保存在堆中的實(shí)例變量
2、保存在方法區(qū)中的類變量。
在java虛擬機(jī)中,每個(gè)對(duì)象和類在邏輯上都是和一個(gè)監(jiān)視器相關(guān)聯(lián)的。對(duì)于對(duì)象來說,相關(guān)聯(lián)的監(jiān)視器保護(hù)對(duì)象的實(shí)例變量。 對(duì)于類來說,監(jiān)視器保護(hù)類的類變量。如果一個(gè)對(duì)象沒有實(shí)例變量,或者說一個(gè)類沒有變量,相關(guān)聯(lián)的監(jiān)視器就什么也不監(jiān)視。
為了實(shí)現(xiàn)監(jiān)視器的排他性監(jiān)視能力,java虛擬機(jī)為每一個(gè)對(duì)象和類都關(guān)聯(lián)一個(gè)鎖。代表任何時(shí)候只允許一個(gè)線程擁有的特權(quán)。線程訪問實(shí)例變量或者類變量不需鎖。 如果某個(gè)線程獲取了鎖,那么在它釋放該鎖之前其他線程是不可能獲取同樣鎖的。一個(gè)線程可以多次對(duì)同一個(gè)對(duì)象上鎖。對(duì)于每一個(gè)對(duì)象,java虛擬機(jī)維護(hù)一個(gè)加鎖計(jì)數(shù)器,線程每獲得一次該對(duì)象,計(jì)數(shù)器就加1,每釋放一次,計(jì)數(shù)器就減 1,當(dāng)計(jì)數(shù)器值為0時(shí),鎖就被完全釋放了。
java編程人員不需要自己動(dòng)手加鎖,對(duì)象鎖是java虛擬機(jī)內(nèi)部使用的。在java程序中,只需要使用synchronized塊或者synchronized方法就可以標(biāo)志一個(gè)監(jiān)視區(qū)域。當(dāng)每次進(jìn)入一個(gè)監(jiān)視區(qū)域時(shí),java 虛擬機(jī)都會(huì)自動(dòng)鎖上對(duì)象或者類。
一個(gè)簡(jiǎn)單的鎖
在使用synchronized時(shí),我們是這樣使用鎖的:
1
2
3
4
5
6
7
|
public class ThreadTest { public void test(){ synchronized ( this ){ //do something } } } |
synchronized可以確保在同一時(shí)間內(nèi)只有一個(gè)線程在執(zhí)行dosomething。下面是使用lock替代synchronized:
1
2
3
4
5
6
7
8
|
public class ThreadTest { Lock lock = new Lock(); public void test(){ lock.lock(); //do something lock.unlock(); } } |
lock()方法會(huì)對(duì)Lock實(shí)例對(duì)象進(jìn)行加鎖,因此所有對(duì)該對(duì)象調(diào)用lock()方法的線程都會(huì)被阻塞,直到該Lock對(duì)象的unlock()方法被調(diào)用。
鎖的是什么?
在這個(gè)問題之前我們必須要明確一點(diǎn):無論synchronized關(guān)鍵字加在方法上還是對(duì)象上,它取得的鎖都是對(duì)象。在java中每一個(gè)對(duì)象都可以作為鎖,它主要體現(xiàn)在下面三個(gè)方面:
對(duì)于同步方法,鎖是當(dāng)前實(shí)例對(duì)象。
對(duì)于同步方法塊,鎖是Synchonized括號(hào)里配置的對(duì)象。
對(duì)于靜態(tài)同步方法,鎖是當(dāng)前對(duì)象的Class對(duì)象。
首先我們先看下面例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class ThreadTest_01 implements Runnable{ @Override public synchronized void run() { for ( int i = 0 ; i < 3 ; i++){ System.out.println(Thread.currentThread().getName() + "run......" ); } } public static void main(String[] args) { for ( int i = 0 ; i < 5 ; i++){ new Thread( new ThreadTest_01(), "Thread_" + i).start(); } } } |
部分運(yùn)行結(jié)果:
1
2
3
4
5
6
7
8
9
|
Thread_2run...... Thread_2run...... Thread_4run...... Thread_4run...... Thread_3run...... Thread_3run...... Thread_3run...... Thread_2run...... Thread_4run...... |
這個(gè)結(jié)果與我們預(yù)期的結(jié)果有點(diǎn)不同(這些線程在這里亂跑),照理來說,run方法加上synchronized關(guān)鍵字后,會(huì)產(chǎn)生同步效果,這些線程應(yīng)該是一個(gè)接著一個(gè)執(zhí)行run方法的。在上面LZ提到,一個(gè)成員方法加上synchronized關(guān)鍵字后,實(shí)際上就是給這個(gè)成員方法加上鎖,具體點(diǎn)就是以這個(gè)成員方法所在的對(duì)象本身作為對(duì)象鎖。但是在這個(gè)實(shí)例當(dāng)中我們一共new了10個(gè)ThreadTest對(duì)象,那個(gè)每個(gè)線程都會(huì)持有自己線程對(duì)象的對(duì)象鎖,這必定不能產(chǎn)生同步的效果。所以:如果要對(duì)這些線程進(jìn)行同步,那么這些線程所持有的對(duì)象鎖應(yīng)當(dāng)是共享且唯一的!
這個(gè)時(shí)候synchronized鎖住的是那個(gè)對(duì)象?它鎖住的就是調(diào)用這個(gè)同步方法對(duì)象。就是說threadTest這個(gè)對(duì)象在不同線程中執(zhí)行同步方法,就會(huì)形成互斥。達(dá)到同步的效果。所以將上面的new Thread(new ThreadTest_01(),”Thread_” + i).start(); 修改為new Thread(threadTest,”Thread_” + i).start();就可以了。
對(duì)于同步方法,鎖是當(dāng)前實(shí)例對(duì)象。
上面實(shí)例是使用synchronized方法,我們?cè)诳纯磗ynchronized代碼塊:
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
|
public class ThreadTest_02 extends Thread{ private String lock ; private String name; public ThreadTest_02(String name,String lock){ this .name = name; this .lock = lock; } @Override public void run() { synchronized (lock) { for ( int i = 0 ; i < 3 ; i++){ System.out.println(name + " run......" ); } } } public static void main(String[] args) { String lock = new String( "test" ); for ( int i = 0 ; i < 5 ; i++){ new ThreadTest_02( "ThreadTest_" + i,lock).start(); } } } |
運(yùn)行結(jié)果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
ThreadTest_0 run...... ThreadTest_0 run...... ThreadTest_0 run...... ThreadTest_1 run...... ThreadTest_1 run...... ThreadTest_1 run...... ThreadTest_4 run...... ThreadTest_4 run...... ThreadTest_4 run...... ThreadTest_3 run...... ThreadTest_3 run...... ThreadTest_3 run...... ThreadTest_2 run...... ThreadTest_2 run...... ThreadTest_2 run...... |
在main方法中我們創(chuàng)建了一個(gè)String對(duì)象lock,并將這個(gè)對(duì)象賦予每一個(gè)ThreadTest2線程對(duì)象的私有變量lock。我們知道java中存在一個(gè)字符串池,那么這些線程的lock私有變量實(shí)際上指向的是堆內(nèi)存中的同一個(gè)區(qū)域,即存放main函數(shù)中的lock變量的區(qū)域,所以對(duì)象鎖是唯一且共享的。線程同步!!
在這里synchronized鎖住的就是lock這個(gè)String對(duì)象。
對(duì)于同步方法塊,鎖是Synchonized括號(hào)里配置的對(duì)象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public class ThreadTest_03 extends Thread{ public synchronized static void test(){ for ( int i = 0 ; i < 3 ; i++){ System.out.println(Thread.currentThread().getName() + " run......" ); } } @Override public void run() { test(); } public static void main(String[] args) { for ( int i = 0 ; i < 5 ; i++){ new ThreadTest_03().start(); } } } |
運(yùn)行結(jié)果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
Thread-0 run...... Thread-0 run...... Thread-0 run...... Thread-4 run...... Thread-4 run...... Thread-4 run...... Thread-1 run...... Thread-1 run...... Thread-1 run...... Thread-2 run...... Thread-2 run...... Thread-2 run...... Thread-3 run...... Thread-3 run...... Thread-3 run...... |
在這個(gè)實(shí)例中,run方法使用的是一個(gè)同步方法,而且是static的同步方法,那么這里synchronized鎖的又是什么呢?我們知道static超脫于對(duì)象之外,它屬于類級(jí)別的。所以,對(duì)象鎖就是該靜態(tài)放發(fā)所在的類的Class實(shí)例。由于在JVM中,所有被加載的類都有唯一的類對(duì)象,在該實(shí)例當(dāng)中就是唯一的 ThreadTest_03.class對(duì)象。不管我們創(chuàng)建了該類的多少實(shí)例,但是它的類實(shí)例仍然是一個(gè)!所以對(duì)象鎖是唯一且共享的。線程同步!!
對(duì)于靜態(tài)同步方法,鎖是當(dāng)前對(duì)象的Class對(duì)象。
如果一個(gè)類中定義了一個(gè)synchronized的static函數(shù)A,也定義了一個(gè)synchronized的instance函數(shù)B,那么這個(gè)類的同一對(duì)象Obj,在多線程中分別訪問A和B兩個(gè)方法時(shí),不會(huì)構(gòu)成同步,因?yàn)樗鼈兊逆i都不一樣。A方法的鎖是Obj這個(gè)對(duì)象,而B的鎖是Obj所屬的那個(gè)Class。
鎖的升級(jí)
java中鎖一共有四種狀態(tài),無鎖狀態(tài),偏向鎖狀態(tài),輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài),它會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí)。鎖可以升級(jí)但不能降級(jí),意味著偏向鎖升級(jí)成輕量級(jí)鎖后不能降級(jí)成偏向鎖。這種鎖升級(jí)卻不能降級(jí)的策略,目的是為了提高獲得鎖和釋放鎖的效率。下面主要部分主要是對(duì)博客:聊聊并發(fā)(二)Java SE1.6中的Synchronized的總結(jié)。
鎖自旋
我們知道在當(dāng)某個(gè)線程在進(jìn)入同步方法/代碼塊時(shí)若發(fā)現(xiàn)該同步方法/代碼塊被其他現(xiàn)在所占,則它就要等待,進(jìn)入阻塞狀態(tài),這個(gè)過程性能是低下的。
在遇到鎖的爭(zhēng)用或許等待事,線程可以不那么著急進(jìn)入阻塞狀態(tài),而是等一等,看看鎖是不是馬上就釋放了,這就是鎖自旋。鎖自旋在一定程度上可以對(duì)線程進(jìn)行優(yōu)化處理。
偏向鎖
偏向鎖主要為了解決在沒有競(jìng)爭(zhēng)情況下鎖的性能問題。在大多數(shù)情況下鎖鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖。當(dāng)某個(gè)線程獲得鎖的情況,該線程是可以多次鎖住該對(duì)象,但是每次執(zhí)行這樣的操作都會(huì)因?yàn)镃AS(CPU的Compare-And-Swap指令)操作而造成一些開銷消耗性能,為了減少這種開銷,這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線程,如果在接下來的執(zhí)行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠(yuǎn)不需要再進(jìn)行同步。
當(dāng)有其他線程在嘗試著競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程就會(huì)釋放鎖。
鎖膨脹
多個(gè)或多次調(diào)用粒度太小的鎖,進(jìn)行加鎖解鎖的消耗,反而還不如一次大粒度的鎖調(diào)用來得高效。
輕量級(jí)鎖
輕量級(jí)鎖能提升程序同步性能的依據(jù)是“對(duì)于絕大部分的鎖,在整個(gè)同步周期內(nèi)都是不存在競(jìng)爭(zhēng)的”,這是一個(gè)經(jīng)驗(yàn)數(shù)據(jù)。輕量級(jí)鎖在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄的空間,用于存儲(chǔ)鎖對(duì)象目前的指向和狀態(tài)。如果沒有競(jìng)爭(zhēng),輕量級(jí)鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競(jìng)爭(zhēng),除了互斥量的開銷外,還額外發(fā)生了CAS操作,因此在有競(jìng)爭(zhēng)的情況下,輕量級(jí)鎖會(huì)比傳統(tǒng)的重量級(jí)鎖更慢。
鎖的公平性
公平性的對(duì)立面是饑餓。那么什么是“饑餓”呢?如果一個(gè)線程因?yàn)槠渌€程在一直搶占著CPU而得不到CPU運(yùn)行時(shí)間,那么我們就稱該線程被“饑餓致死”。而解決饑餓的方案則被稱之為“公平性”——所有線程均可以公平地獲得CPU運(yùn)行機(jī)會(huì)。
導(dǎo)致線程饑餓主要有如下幾個(gè)原因:
高優(yōu)先級(jí)線程吞噬所有的低優(yōu)先級(jí)線程的CPU時(shí)間。我們可以為每個(gè)線程單獨(dú)設(shè)置其優(yōu)先級(jí),從1到10。優(yōu)先級(jí)越高的線程獲得CPU的時(shí)間越多。對(duì)大多數(shù)應(yīng)用來說,我們最好是不要改變其優(yōu)先級(jí)值。
線程被永久堵塞在一個(gè)等待進(jìn)入同步塊的狀態(tài)。java的同步代碼區(qū)是導(dǎo)致線程饑餓的重要因素。java的同步代碼塊并不會(huì)保證進(jìn)入它的線程的先后順序。這就意味著理論上存在一個(gè)或者多個(gè)線程在試圖進(jìn)入同步代碼區(qū)時(shí)永遠(yuǎn)被堵塞著,因?yàn)槠渌€程總是不斷優(yōu)于他獲得訪問權(quán),導(dǎo)致它一直得到不到CPU運(yùn)行機(jī)會(huì)被“饑餓致死”。
線程在等待一個(gè)本身也處于永久等待完成的對(duì)象。如果多個(gè)線程處在wait()方法執(zhí)行上,而對(duì)其調(diào)用notify()不會(huì)保證哪一個(gè)線程會(huì)獲得喚醒,任何線程都有可能處于繼續(xù)等待的狀態(tài)。因此存在這樣一個(gè)風(fēng)險(xiǎn):一個(gè)等待線程從來得不到喚醒,因?yàn)槠渌却€程總是能被獲得喚醒。
為了解決線程“饑餓”的問題,我們可以使用鎖實(shí)現(xiàn)公平性。
鎖的可重入性
我們知道當(dāng)線程請(qǐng)求一個(gè)由其它線程持有鎖的對(duì)象時(shí),該線程會(huì)阻塞,但是當(dāng)線程請(qǐng)求由自己持有鎖的對(duì)象時(shí),是否可以成功呢?答案是可以成功的,成功的保障就是線程鎖的“可重入性”。
“可重入”意味著自己可以再次獲得自己的內(nèi)部鎖,而不需要阻塞。如下:
1
2
3
4
5
6
7
8
9
10
11
|
public class Father { public synchronized void method(){ //do something } } public class Child extends Father{ public synchronized void method(){ //do something super .method(); } } |
如果所是不可重入的,上面的代碼就會(huì)死鎖,因?yàn)檎{(diào)用child的method(),首先會(huì)獲取父類Father的內(nèi)置鎖然后獲取Child的內(nèi)置鎖,當(dāng)調(diào)用父類的方法時(shí),需要再次后去父類的內(nèi)置鎖,如果不可重入,可能會(huì)陷入死鎖。
java多線程的可重入性的實(shí)現(xiàn)是通過每個(gè)鎖關(guān)聯(lián)一個(gè)請(qǐng)求計(jì)算和一個(gè)占有它的線程,當(dāng)計(jì)數(shù)為0時(shí),認(rèn)為該鎖是沒有被占有的,那么任何線程都可以獲得該鎖的占有權(quán)。當(dāng)某一個(gè)線程請(qǐng)求成功后,JVM會(huì)記錄該鎖的持有線程 并且將計(jì)數(shù)設(shè)置為1,如果這時(shí)其他線程請(qǐng)求該鎖時(shí)則必須等待。當(dāng)該線程再次請(qǐng)求請(qǐng)求獲得鎖時(shí),計(jì)數(shù)會(huì)+1;當(dāng)占有線程退出同步代碼塊時(shí),計(jì)數(shù)就會(huì)-1,直到為0時(shí),釋放該鎖。這時(shí)其他線程才有機(jī)會(huì)獲得該鎖的占有權(quán)。
lock及其實(shí)現(xiàn)類
java.util.concurrent.locks提供了非常靈活鎖機(jī)制,為鎖定和等待條件提供一個(gè)框架的接口和類,它不同于內(nèi)置同步和監(jiān)視器,該框架允許更靈活地使用鎖定和條件。它的類結(jié)構(gòu)圖如下:
ReentrantLock:一個(gè)可重入的互斥鎖,為lock接口的主要實(shí)現(xiàn)。
ReentrantReadWriteLock:
ReadWriteLock:ReadWriteLock 維護(hù)了一對(duì)相關(guān)的鎖,一個(gè)用于只讀操作,另一個(gè)用于寫入操作。
Semaphore:一個(gè)計(jì)數(shù)信號(hào)量。
Condition:鎖的關(guān)聯(lián)條件,目的是允許線程獲取鎖并且查看等待的某一個(gè)條件是否滿足。
CyclicBarrier:一個(gè)同步輔助類,它允許一組線程互相等待,直到到達(dá)某個(gè)公共屏障點(diǎn)。