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