線程是程序執(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í)資源绳锅,目前受益良多西饵。