干貨:Java多線程詳解(內(nèi)附源碼)

線程是程序執(zhí)行的最小單元筋粗,多線程是指程序同一時間可以有多個執(zhí)行單元運行(這個與你的CPU核心有關(guān))骚腥。

在java中開啟一個新線程非常簡單敦间,創(chuàng)建一個Thread對象,然后調(diào)用它的start方法桦沉,一個新線程就開啟了每瞒。

那么執(zhí)行代碼放在那里呢金闽?有兩種方式:1. 創(chuàng)建Thread對象時纯露,復(fù)寫它的run方法,把執(zhí)行代碼放在run方法里代芜。2. 創(chuàng)建Thread對象時埠褪,給它傳遞一個Runnable對象,把執(zhí)行代碼放在Runnable對象的run方法里。

如果多線程操作的是不同資源钞速,線程之間不會相互影響贷掖,不會產(chǎn)生任何問題。但是如果多線程操作相同資源(共享變量)渴语,就會產(chǎn)生多線程沖突苹威,要知道這些沖突產(chǎn)生的原因,就要先了解java內(nèi)存模型(簡稱JMM)驾凶。

一. java內(nèi)存模型(JMM)

1.1 java內(nèi)存模型(JMM)介紹

java內(nèi)存模型決定一個線程對共享變量的寫入何時對另一個線程可見牙甫。從抽樣的角度來說:線程之間的共享變量存儲在主內(nèi)存(main memory)中,每個線程都有一個私有的本地內(nèi)存(local memory)调违,本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本窟哺。

存在兩種內(nèi)存:主內(nèi)存和線程本地內(nèi)存,線程開始時技肩,會復(fù)制一份共享變量的副本放在本地內(nèi)存中且轨。

線程對共享變量操作其實都是操作線程本地內(nèi)存中的副本變量,當(dāng)副本變量發(fā)生改變時虚婿,線程會將它刷新到主內(nèi)存中(并不一定立即刷新旋奢,何時刷新由線程自己控制)。

當(dāng)主內(nèi)存中變量發(fā)生改變然痊,就會通知發(fā)出信號通知其他線程將該變量的緩存行置為無效狀態(tài)黄绩,因此當(dāng)其他線程從本地內(nèi)存讀取這個變量時,發(fā)現(xiàn)這個變量已經(jīng)無效了玷过,那么它就會從內(nèi)存重新讀取爽丹。

1.2 可見性

從上面的介紹中,我們看出多線程操作共享變量辛蚊,會產(chǎn)生一個問題粤蝎,那就是可見性問題: 即一個線程對共享變量修改,對另一個線程來說并不是立即可見的袋马。

classData{inta =0;intb =0;intx =0;inty =0;// a線程執(zhí)行publicvoidthreadA(){? ? ? ? a =1;? ? ? ? x = b;? ? }// b線程執(zhí)行publicvoidthreadB(){? ? ? ? b =2;? ? ? ? y = a;? ? }}

如果有兩個線程同時分別執(zhí)行了threadA和threadB方法初澎。可能會出現(xiàn)x==y==0這個情況(當(dāng)然這個情況比較少的出現(xiàn))虑凛。

因為a和b被賦值后碑宴,還沒有刷新到主內(nèi)存中,就執(zhí)行x = b和y = a的語句桑谍,這個時候線程并不知道a和b還已經(jīng)被修改了延柠,依然是原來的值0。

1.3 有序性

為了提高程序執(zhí)行性能锣披,Java內(nèi)存模型允許編譯器和處理器對指令進行重排序贞间。重排序過程不會影響到單線程程序的執(zhí)行贿条,卻會影響到多線程并發(fā)執(zhí)行的正確性。

classReorder{intx =0;booleanflag =false;publicvoidwriter(){? ? ? ? x =1;? ? ? ? flag =true;? ? }publicvoidreader(){if(flag) {inta = x * x;? ? ? ? ? ? ...? ? ? ? }? ? }}

例如上例中增热,我們使用flag變量整以,標志x變量已經(jīng)被賦值了。但是這兩個語句之間沒有數(shù)據(jù)依賴峻仇,所以它們可能會被重排序公黑,即flag = true語句會在x = 1語句之前,那么這么更改會不會產(chǎn)生問題呢摄咆?

在單線程模式下帆调,不會有任何問題,因為writer方法是一個整體豆同,只有等writer方法執(zhí)行完畢番刊,其他方法才能執(zhí)行,所以flag = true語句和x = 1語句順序改變沒有任何影響影锈。

在多線程模式下芹务,就可能會產(chǎn)生問題,因為writer方法還沒有執(zhí)行完畢鸭廷,reader方法就被另一線程調(diào)用了枣抱,這個時候如果flag = true語句和x = 1語句順序改變,就有可能產(chǎn)生flag為true辆床,但是x還沒有賦值情況佳晶,與程序意圖產(chǎn)生不一樣,就會產(chǎn)生意想不到的問題讼载。

1.4 原子性

在Java中轿秧,對基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的咨堤,要么執(zhí)行菇篡,要么不執(zhí)行。

x =1;// 原子性y = x;// 不是原子性x = x +1;// 不是原子性x++;// 不是原子性System.out.println(x);// 原子性

公式2:有兩個原子性操作一喘,讀取x的值驱还,賦值給y。公式3:也是三個原子性操作凸克,讀取x的值议蟆,加1,賦值給x萎战。公式4:和公式3一樣咐容。

所以對于原子性操作就兩種:1. 將基本數(shù)據(jù)類型常量賦值給變量。2. 讀取基本數(shù)據(jù)類型的變量值撞鹉。任何計算操作都不是原子的疟丙。

1.5 小結(jié)

多線程操作共享變量颖侄,會產(chǎn)生上面三個問題鸟雏,可見性享郊、有序性和原子性。

可見性: 一個線程改變共享變量孝鹊,可能并沒有立即刷新到主內(nèi)存炊琉,這個時候另一個線程讀取共享變量,就是改變之前的值又活。所以這個共享變量的改變對其他線程并不是可見的苔咪。

有序性: 編譯器和處理器會對指令進行重排序,語句的順序發(fā)生改變柳骄,這樣在多線程的情況下团赏,可能出現(xiàn)奇怪的異常。

原子性: 只有對基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作耐薯。

要解決這三個問題有兩種方式:

volatile關(guān)鍵字:它只能解決兩個問題可見性和有序性問題舔清,但是如果volatile修飾基本數(shù)據(jù)類型變量,而且這個變量只做讀取和賦值操作曲初,那么也沒有原子性問題了体谒。比如說用它來修飾boolean的變量。

加鎖:可以保證同一時間只有同一線程操作共享變量臼婆,當(dāng)前線程操作共享變量時抒痒,共享變量不會被別的線程修改,所以可見性颁褂、有序性和原子性問題都得到解決故响。分為synchronized同步鎖和JUC框架下的Lock鎖。

二. volatile關(guān)鍵字

volatile關(guān)鍵字作用

1.可見性: 對一個volatile變量的讀取颁独,總是能看到(任意線程)對這個volatile變量最后的寫入被去。

有序性: 禁止指令重排序,即在程序中在volatile變量進行操作時,在其之前的操作肯定已經(jīng)全部執(zhí)行了奖唯,而且結(jié)果已經(jīng)對后面的操作可見惨缆,在其之后的操作肯定還沒有執(zhí)行。

這個的具體解釋丰捷,大家請看《深入理解Java內(nèi)存模型》里面關(guān)于happens-before規(guī)則的講解坯墨。

classVolatileFeaturesExample{//使用volatile聲明一個基本數(shù)據(jù)類型變量vlvolatilelongvl =0L;//對于單個volatile基本數(shù)據(jù)類型變量賦值publicvoidset(longl){? ? ? ? vl = l;? ? }//對于單個volatile基本數(shù)據(jù)類型變量的復(fù)合操作publicvoidgetAndIncrement(){? ? ? ? vl++;? ? }//對于單個volatile基本數(shù)據(jù)類型變量讀取publiclongget(){returnvl;? ? }}classVolatileFeaturesExample{//聲明一個基本數(shù)據(jù)類型變量vllongvl =0L;// 相當(dāng)于加了同步鎖publicsynchronizedvoidset(longl){? ? ? vl = l;? ? }// 普通方法publicvoidgetAndIncrement(){longtemp = get();? ? ? ? temp +=1L;? ? ? ? set(temp);? ? }// 相當(dāng)于加了同步鎖publicsynchronizedlongget(){returnvl;? ? }}

如果volatile修飾基本數(shù)據(jù)類型變量,而且只對這個變量做讀取和賦值操作病往,那么就相當(dāng)于加了同步鎖捣染。

三. synchronized同步鎖

synchronized同步鎖作用是訪問被鎖住的資源時,只要獲取鎖的線程才能操作被鎖住的資源停巷,其他線程必須阻塞等待耍攘。

所以一個線程來說榕栏,可以阻塞等待,可以運行蕾各,那么線程到底有哪些狀態(tài)呢扒磁?

3.1 線程狀態(tài)

狀態(tài)轉(zhuǎn)換圖

線程分為5種狀態(tài):

新建狀態(tài)(New):創(chuàng)建一個Thread對象,那么該thread對象就是新建狀態(tài)式曲。

可運行狀態(tài)(Runnable):表示該thread線程隨時都可以運行妨托,只要獲取CPU的執(zhí)行權(quán)。?

注: 該狀態(tài)可以由新建狀態(tài)轉(zhuǎn)換而來(通過調(diào)用thread的start方法)吝羞,也可以由阻塞狀態(tài)轉(zhuǎn)換而來

運行狀態(tài)(Running):表示該線程正在運行兰伤,注意運行狀態(tài)只能從可運行狀態(tài)到達。

阻塞狀態(tài)(Blocked):表示該線程當(dāng)前停止運行钧排,主要分為三種情況:?

1). 同步阻塞狀態(tài):線程獲取同步鎖失敗敦腔,就會進入同步阻塞狀態(tài)。?

2). 等待阻塞狀態(tài):線程調(diào)用wait方法恨溜,進入該狀態(tài)符衔。注:join方法本質(zhì)也是通過wait方法實現(xiàn)的。?

3). 其他阻塞狀態(tài):通過Thread.sleep方法讓線程睡眠筒捺,開啟IO流讓線程等待阻塞柏腻。

死亡狀態(tài)(Dead):當(dāng)thread的run方法運行完畢,那么線程就進入死亡狀態(tài)系吭。該狀態(tài)不能再轉(zhuǎn)換成其他狀態(tài)五嫂。

3.2 synchronized同步方法或者同步塊

synchronized同步方法或者同步塊具體是怎樣操作的呢?

相當(dāng)于有一個大房間肯尺,房間門上有一把鎖lock沃缘,房間里面存放的是所有與這把鎖lock關(guān)聯(lián)的同步方法或者同步塊。

當(dāng)某一個線程要執(zhí)行這把鎖lock的一個同步方法或者同步塊時则吟,它就來到房間門前槐臀,如果發(fā)現(xiàn)鎖lock還在,那么它就拿著鎖進入房間氓仲,并將房間鎖上水慨,它可以執(zhí)行房間中任何一個同步方法或者同步塊。

這時又有另一個線程要執(zhí)行這把鎖lock的一個同步方法或者同步塊時敬扛,它就來到房間門前晰洒,發(fā)現(xiàn)鎖lock沒有了,就只能在門外等待啥箭,此時該線程就在synchronized同步阻塞線程池中谍珊。

等到拿到鎖lock的線程,同步方法或者同步塊代碼執(zhí)行完畢急侥,它就會從房間中退出來砌滞,將鎖放到門上侮邀。

這時在門外等待的線程就爭奪這把鎖lock,拿到鎖的線程就可以進入房間贝润,其他線程則又要繼續(xù)等待绊茧。

注:synchronized 鎖是鎖住所有與這個鎖關(guān)聯(lián)的同步方法或者同步塊。

synchronized的同步鎖到底是什么呢题暖?

其實就是java對象按傅,在Java中捉超,每一個對象都擁有一個鎖標記(monitor)胧卤,也稱為監(jiān)視器,多線程同時訪問某個對象時拼岳,線程只有獲取了該對象的鎖才能訪問枝誊。

3.3 wait與notify、notifyAll

這三個方法主要用于實現(xiàn)線程之間相互等待的問題惜纸。

調(diào)用對象lock的wait方法叶撒,會讓當(dāng)前線程進行等待,即將當(dāng)前線程放入對象lock的線程等待池中耐版。調(diào)用對象lock的notify方法會從線程等待池中隨機喚醒一個線程祠够,notifyAll方法會喚醒所有線程。

注:對象lock的wait與notify粪牲、notifyAll方法調(diào)用必須放在以對象lock為鎖的同步方法或者同步塊中古瓤,否則會拋出IllegalMonitorStateException異常。

wait與notify腺阳、notifyAll具體是怎么操作的呢落君?

前面過程與synchronized中介紹的一樣,當(dāng)調(diào)用鎖lock的wait方法時亭引,該線程(即當(dāng)前線程)退出房間绎速,歸還鎖lock,但并不是進入synchronized同步阻塞線程池中焙蚓,而是進入鎖lock的線程等待池中纹冤。

這時另一個線程拿到鎖lock進行房間,如果它執(zhí)行了鎖lock的notify方法购公,那么就會從鎖lock的線程等待池中隨機喚醒一個線程萌京,將它放入synchronized同步阻塞線程池中(記住只有拿到鎖lock的線程才能進行房間)。調(diào)用鎖lock的notifyAll方法君丁,即喚醒線程等待池所有線程枫夺。

注:當(dāng)被wait阻塞的線程再次進入synchronized同步代碼塊時,會從wait方法調(diào)用之后的地方繼續(xù)執(zhí)行绘闷。

在鎖lock的線程等待池中的線程橡庞,只有四種方式喚醒:

通過notify()喚醒

通過notifyAll()喚醒

通過interrupt()中斷喚醒

如果是通過調(diào)用wait(long timeout)進入等待狀態(tài)的線程较坛,當(dāng)時間超時的時候,也會被喚醒扒最。

注意wait丑勤、notify和notifyAll方法必須先獲取鎖才能調(diào)用,否則拋出IllegalMonitorStateException異常吧趣。而只有synchronized模塊才能讓當(dāng)前線程獲取鎖法竞,所以wait方法只能在synchronized模塊中執(zhí)行。

四. 其他重要方法

4.1 join方法

讓當(dāng)前線程等待另一個線程執(zhí)行完成后强挫,才繼續(xù)執(zhí)行岔霸。

publicfinalvoidjoin()throwsInterruptedException {join(0);? ? }publicfinalsynchronizedvoidjoin(longmillis)throwsInterruptedException {// 獲取當(dāng)前系統(tǒng)毫秒數(shù)longbase = System.currentTimeMillis();longnow =0;// millis小于0,拋出異常if(millis <0) {thrownewIllegalArgumentException("timeout value is negative");? ? ? ? }if(millis ==0) {// 通過isAlive判斷當(dāng)前線程是否存活while(isAlive()) {// wait(0)表示當(dāng)前線程無限等待wait(0);? ? ? ? ? ? }? ? ? ? }else{// 通過isAlive判斷當(dāng)前線程是否存活while(isAlive()) {longdelay = millis - now;if(delay <=0) {break;? ? ? ? ? ? ? ? }// 當(dāng)前線程等待delay毫秒俯渤,超過時間呆细,當(dāng)前線程就被喚醒wait(delay);? ? ? ? ? ? ? ? now = System.currentTimeMillis() - base;? ? ? ? ? ? }? ? ? ? }? ? }

join方法是Thread中的方法,synchronized方法同步的鎖對象就是Thread對象八匠,通過調(diào)用Thread對象的wait方法絮爷,讓當(dāng)前線程等待

注意:這里是讓當(dāng)前線程等待,即當(dāng)前調(diào)用join方法的線程梨树,而不是Thread對象的線程坑夯。那么當(dāng)前線程什么時候會被喚醒呢?

當(dāng)Thread對象線程執(zhí)行完畢抡四,進入死亡狀態(tài)時柜蜈,會調(diào)用Thread對象的notifyAll方法,來喚醒Thread對象的線程等待池中所有線程床嫌。

示例:

publicstaticvoidjoinTest(){? ? ? ? Thread thread =newThread(newRunnable() {? ? ? ? ? ? @Overridepublicvoidrun(){for(inti =0; i <10; i++) {try{? ? ? ? ? ? ? ? ? ? ? ? Thread.sleep(100);? ? ? ? ? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName()+":? i==="+i);? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? },"t1");? ? ? ? thread.start();try{? ? ? ? ? ? thread.join();? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? e.printStackTrace();? ? ? ? }? ? ? ? System.out.println(Thread.currentThread().getName()+": end");? ? }

4.2 sleep方法

只是讓當(dāng)前線程等待一定的時間跨释,才繼續(xù)執(zhí)行。

4.3 yield方法

將當(dāng)前線程狀態(tài)從運行狀態(tài)轉(zhuǎn)成可運行狀態(tài)厌处,如果再獲取CPU執(zhí)行權(quán)鳖谈,就繼續(xù)執(zhí)行。

4.4 interrupt方法

中斷線程阔涉,它會中斷處于阻塞狀態(tài)下的線程缆娃,但是對于運行狀態(tài)下的線程不起任何作用。

示例:

publicstaticvoidinterruptTest(){// 處于阻塞狀態(tài)下的線程Thread thread =newThread(newRunnable() {? ? ? ? ? ? @Overridepublicvoidrun(){try{? ? ? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName()+" 開始");? ? ? ? ? ? ? ? ? ? Thread.sleep(1000);? ? ? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName()+" 結(jié)束");? ? ? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName()+" 產(chǎn)生異常");? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? },"t1");? ? ? ? thread.start();// 處于運行狀態(tài)下的線程Thread thread1 =newThread(newRunnable() {? ? ? ? ? ? @Overridepublicvoidrun(){? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName()+" 開始");inti =0;while(i < Integer.MAX_VALUE -10) {? ? ? ? ? ? ? ? ? ? i = i +1;for(intj =0; j < i; j++);? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName()+" i=="+i);? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName()+" 結(jié)束");? ? ? ? ? ? }? ? ? ? },"t2");? ? ? ? thread1.start();try{? ? ? ? ? ? Thread.sleep(10);? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? e.printStackTrace();? ? ? ? }? ? ? ? System.out.println(Thread.currentThread().getName()+" 進行中斷");? ? ? ? thread.interrupt();? ? ? ? thread1.interrupt();? ? }

4.5 isInterrupted方法

返回這個線程是否被中斷瑰排。注意當(dāng)調(diào)用線程的interrupt方法后贯要,該線程的isInterrupted的方法就會返回true。如果異常被處理了椭住,又會將該標志位置位false崇渗,即isInterrupted的方法返回false。

4.6 線程優(yōu)先級以及守護線程

在java中線程優(yōu)先級范圍是1~10,默認的優(yōu)先級是5宅广。

在java中線程分為用戶線程和守護線程葫掉,isDaemon返回是true,表示它是守護線程跟狱。當(dāng)所有的用戶線程執(zhí)行完畢后俭厚,java虛擬機就會退出,不管是否還有守護線程未執(zhí)行完畢驶臊。

當(dāng)創(chuàng)建一個新線程時挪挤,這個新線程的優(yōu)先級等于創(chuàng)建它線程的優(yōu)先級,且只有當(dāng)創(chuàng)建它線程是守護線程時关翎,新線程才是守護線程扛门。

當(dāng)然也可以通過setPriority方法修改線程的優(yōu)先級,已經(jīng)setDaemon方法設(shè)置線程是否為守護線程笤休。

五. 實例講解

5.1 不加任何同步鎖

importjava.util.Collections;importjava.util.List;importjava.util.concurrent.CopyOnWriteArrayList;importjava.util.concurrent.CountDownLatch;classData {intnum;publicData(intnum){this.num = num;? ? }publicintgetAndDecrement(){returnnum--;? ? }}classMyRun implements Runnable {privateData data;// 用來記錄所有賣出票的編號privateListlist;privateCountDownLatch latch;publicMyRun(Data data, Listlist, CountDownLatch latch){this.data = data;this.list=list;this.latch = latch;? ? }? ? @Overridepublicvoidrun(){try{? ? ? ? ? ? action();? ? ? ? }? finally {// 釋放latch共享鎖latch.countDown();? ? ? ? }? ? }// 進行買票操作尖飞,注意這里沒有使用data.num>0作為判斷條件症副,直到賣完線程退出店雅。// 那么做會導(dǎo)致這兩處使用了共享變量data.num,那么做多線程同步時贞铣,就要考慮更多條件闹啦。// 這里只for循環(huán)了5次,表示每個線程只賣5張票辕坝,并將所有賣出去編號存入list集合中窍奋。publicvoidaction(){for(inti =0; i <5; i++) {try{? ? ? ? ? ? ? ? Thread.sleep(10);? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? }intnewNum = data.getAndDecrement();? ? ? ? ? ? System.out.println("線程"+Thread.currentThread().getName()+"? num=="+newNum);list.add(newNum);? ? ? ? }? ? }}publicclassThreadTest {publicstaticvoidstartThread(Data data, String name, Listlist,CountDownLatch latch){? ? ? ? Thread t =newThread(newMyRun(data,list, latch), name);? ? ? ? t.start();? ? }publicstaticvoidmain(String[] args){// 使用CountDownLatch來讓主線程等待子線程都執(zhí)行完畢時,才結(jié)束CountDownLatch latch =newCountDownLatch(6);longstart = System.currentTimeMillis();// 這里用并發(fā)list集合Listlist=newCopyOnWriteArrayList();? ? ? ? Data data =newData(30);? ? ? ? startThread(data,"t1",list, latch);? ? ? ? startThread(data,"t2",list, latch);? ? ? ? startThread(data,"t3",list, latch);? ? ? ? startThread(data,"t4",list, latch);? ? ? ? startThread(data,"t5",list, latch);? ? ? ? startThread(data,"t6",list, latch);try{? ? ? ? ? ? latch.await();? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? e.printStackTrace();? ? ? ? }// 處理一下list集合酱畅,進行排序和翻轉(zhuǎn)Collections.sort(list);? ? ? ? Collections.reverse(list);? ? ? ? System.out.println(list);longtime = System.currentTimeMillis() - start;// 輸出一共花費的時間System.out.println("\n主線程結(jié)束 time=="+time);? ? }}

輸出的結(jié)果是

線程t2num==29線程t6num==27線程t5num==28線程t4num==28線程t1num==30線程t3num==30線程t2num==26線程t4num==24線程t6num==25線程t5num==23線程t1num==22線程t3num==21線程t4num==20線程t6num==19線程t5num==18線程t2num==17線程t1num==16線程t3num==15線程t4num==14線程t5num==12線程t6num==13線程t1num==9線程t3num==10線程t2num==11線程t1num==8線程t6num==5線程t2num==7線程t5num==3線程t3num==4線程t4num==6[30,30,29,28,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3]主線程結(jié)束 time==62

從結(jié)果中發(fā)現(xiàn)問題琳袄,出現(xiàn)了重復(fù)票,所以30張票沒有被賣完纺酸。最主要的原因就是Data類的getAndDecrement方法操作不是多線程安全的窖逗。

首先它不能保證原子性,分為三個操作餐蔬,先讀取num的值碎紊,然后num自減,在返回自減前的值樊诺。

因為num不是volatile關(guān)鍵字修飾的仗考,它也不能保證可見性和有序性。

所以只要保證getAndDecrement方法多線程安全词爬,那么就可以解決上面出現(xiàn)的問題秃嗜。那么保證getAndDecrement方法多線程安全呢?最簡單的方式就是在getAndDecrement方法前加synchronized關(guān)鍵字。

這是synchronized關(guān)鍵鎖就是這個data對象實例锅锨,所以保證了多線程調(diào)用getAndDecrement方法時螺句,只有一個線程能調(diào)用,等待調(diào)用完成橡类,其他線程才能調(diào)用getAndDecrement方法蛇尚。

因為同一時間只有一個線程調(diào)用getAndDecrement方法,所以它在做num--操作時顾画,不用擔(dān)心num變量會發(fā)生改變取劫。所以原子性、可見性和有序性都可以得到保證研侣。

5.2 使用最小同步鎖

classData{intnum;? ? public Data(intnum) {this.num=num;? ? }// 將getAndDecrement方法加了同步鎖public synchronizedintgetAndDecrement() {returnnum--;? ? }}

輸出結(jié)果

線程t1num==30線程t2num==29線程t6num==28線程t4num==26線程t3num==27線程t5num==25線程t6num==22線程t2num==21線程t3num==23線程t1num==24線程t4num==20線程t5num==19線程t2num==18線程t3num==17線程t5num==13線程t4num==14線程t6num==16線程t1num==15線程t2num==12線程t4num==9線程t1num==7線程t5num==10線程t3num==11線程t6num==8線程t4num==6線程t2num==3線程t1num==2線程t3num==4線程t5num==5線程t6num==1[30,29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1]主線程結(jié)束 time==61

我們只是將Data的getAndDecrement方法加了同步鎖谱邪,發(fā)現(xiàn)解決了多線程并發(fā)問題。主要是因為我們只在一處使用了共享變量num庶诡,所以只需要將這處加同步就行了惦银。而且你會發(fā)現(xiàn)最后花費的總時間與沒加同步鎖時幾乎一樣,那么因為我們同步代碼足夠小末誓。

相反地扯俱,我們加地同步鎖不合理,可能也能實現(xiàn)多線程安全喇澡,但是耗時就會大大增加迅栅。

5.3 不合理地使用同步鎖

@Overridepublicvoidrun(){try{synchronized(data){? ? ? ? ? ? ? ? action();? ? ? ? ? ? }? ? ? ? }finally{// 釋放latch共享鎖latch.countDown();? ? ? ? }? ? }

輸入結(jié)果:

線程t1num==30線程t1num==29線程t1num==28線程t1num==27線程t1num==26線程t6num==25線程t6num==24線程t6num==23線程t6num==22線程t6num==21線程t5num==20線程t5num==19線程t5num==18線程t5num==17線程t5num==16線程t4num==15線程t4num==14線程t4num==13線程t4num==12線程t4num==11線程t3num==10線程t3num==9線程t3num==8線程t3num==7線程t3num==6線程t2num==5線程t2num==4線程t2num==3線程t2num==2線程t2num==1[30,29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1]主線程結(jié)束 time==342

在這里我們將整個action方法,放入同步代碼塊中晴玖,也可以解決多線程沖突問題读存,但是所耗費的時間是在getAndDecrement方法上加同步鎖時間的幾倍。

所以我們在加同步鎖的時候呕屎,那些需要同步让簿,就是看那些地方使用了共享變量。比如這里只在getAndDecrement方法中使用了同步變量秀睛,所以只要給它加鎖就行了尔当。

但是如果在action方法中,使用data.num>0來作為循環(huán)條件琅催,那么在加同步鎖時居凶,就必須將整個action方法放在同步模塊中,因為我們必須保證藤抡,在data.num>0判斷到getAndDecrement方法調(diào)用這些代碼都是在同步模塊中侠碧,不然就會產(chǎn)生多線程沖突問題。

福利:

想要了解更多多線程知識點的缠黍,可以關(guān)注我一下弄兜,我后續(xù)也會整理更多關(guān)于多線程這一塊的知識點分享出來,另外順便給大家推薦一個交流學(xué)習(xí)群:650385180,里面會分享一些資深架構(gòu)師錄制的視頻錄像:有Spring替饿,MyBatis语泽,Netty源碼分析,高并發(fā)视卢、高性能踱卵、分布式、多線程据过、微服務(wù)架構(gòu)的原理惋砂,JVM性能優(yōu)化這些成為架構(gòu)師必備的知識體系。還能領(lǐng)取免費的學(xué)習(xí)資源绳锅,目前受益良多西饵。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市鳞芙,隨后出現(xiàn)的幾起案子眷柔,更是在濱河造成了極大的恐慌,老刑警劉巖原朝,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驯嘱,死亡現(xiàn)場離奇詭異,居然都是意外死亡竿拆,警方通過查閱死者的電腦和手機宙拉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丙笋,“玉大人,你說我怎么就攤上這事煌贴∮澹” “怎么了?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵牛郑,是天一觀的道長怠肋。 經(jīng)常有香客問我,道長淹朋,這世上最難降的妖魔是什么笙各? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮础芍,結(jié)果婚禮上杈抢,老公的妹妹穿的比我還像新娘。我一直安慰自己仑性,他們只是感情好惶楼,可當(dāng)我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般歼捐。 火紅的嫁衣襯著肌膚如雪何陆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天豹储,我揣著相機與錄音贷盲,去河邊找鬼。 笑死剥扣,一個胖子當(dāng)著我的面吹牛晃洒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播朦乏,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼球及,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了呻疹?” 一聲冷哼從身側(cè)響起莽鸭,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎闯参,沒想到半個月后朽砰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡并思,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年庐氮,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宋彼。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡弄砍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出输涕,到底是詐尸還是另有隱情音婶,我是刑警寧澤,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布莱坎,位于F島的核電站衣式,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏檐什。R本人自食惡果不足惜碴卧,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望乃正。 院中可真熱鬧住册,春花似錦、人聲如沸烫葬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至垢箕,卻和暖如春划栓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背条获。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工忠荞, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人帅掘。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓委煤,卻偏偏與公主長得像,于是被迫代替她去往敵國和親修档。 傳聞我的和親對象是個殘疾皇子碧绞,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容

  • Java多線程學(xué)習(xí) [-] 一擴展javalangThread類 二實現(xiàn)javalangRunnable接口 三T...
    影馳閱讀 2,952評論 1 18
  • 本文主要講了java中多線程的使用方法、線程同步吱窝、線程數(shù)據(jù)傳遞讥邻、線程狀態(tài)及相應(yīng)的一些線程函數(shù)用法、概述等院峡。 首先講...
    李欣陽閱讀 2,444評論 1 15
  • 該文章轉(zhuǎn)自:http://blog.csdn.net/evankaka/article/details/44153...
    加來依藍閱讀 7,338評論 3 87
  • 1.解決信號量丟失和假喚醒 public class MyWaitNotify3{ MonitorObject m...
    Q羅閱讀 873評論 0 1
  • 一直覺得自己是個很堅強的人兴使,堅強到所有困難都一個人扛住,可是此刻我卻迸發(fā)了一種強烈的不安全感照激,我突然很害怕肚里的寶...
    八戒55555閱讀 262評論 0 0