線程是程序執(zhí)行的最小單元,多線程是指程序同一時間可以有多個執(zhí)行單元運行(這個與你的CPU核心有關)莉恼。
在java中開啟一個新線程非常簡單妨托,創(chuàng)建一個Thread對象陈轿,然后調(diào)用它的start方法,一個新線程就開啟了链沼。
那么執(zhí)行代碼放在那里呢默赂?有兩種方式:1. 創(chuàng)建Thread對象時,復寫它的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)存蜡峰,線程開始時,會復制一份共享變量的副本放在本地內(nèi)存中朗恳。
- 線程對共享變量操作其實都是操作線程本地內(nèi)存中的副本變量湿颅,當副本變量發(fā)生改變時,線程會將它刷新到主內(nèi)存中(并不一定立即刷新粥诫,何時刷新由線程自己控制)油航。
- 當主內(nèi)存中變量發(fā)生改變,就會通知發(fā)出信號通知其他線程將該變量的緩存行置為無效狀態(tài)怀浆,因此當其他線程從本地內(nèi)存讀取這個變量時谊囚,發(fā)現(xiàn)這個變量已經(jīng)無效了,那么它就會從內(nèi)存重新讀取执赡。
1.2 可見性
從上面的介紹中镰踏,我們看出多線程操作共享變量,會產(chǎn)生一個問題搀玖,那就是可見性問題: 即一個線程對共享變量修改余境,對另一個線程來說并不是立即可見的。
class Data {
int a = 0;
int b = 0;
int x = 0;
int y = 0;
// a線程執(zhí)行
public void threadA() {
a = 1;
x = b;
}
// b線程執(zhí)行
public void threadB() {
b = 2;
y = a;
}
}
如果有兩個線程同時分別執(zhí)行了threadA和threadB方法灌诅》祭矗可能會出現(xiàn)x==y==0這個情況(當然這個情況比較少的出現(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í)行的正確性。
class Reorder {
int x = 0;
boolean flag = false;
public void writer() {
x = 1;
flag = true;
}
public void reader() {
if (flag) {
int a = 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 小結
多線程操作共享變量,會產(chǎn)生上面三個問題旋廷,可見性饶碘、有序性和原子性馒吴。
- 可見性: 一個線程改變共享變量饮戳,可能并沒有立即刷新到主內(nèi)存,這個時候另一個線程讀取共享變量鬼吵,就是改變之前的值齿椅。所以這個共享變量的改變對其他線程并不是可見的涣脚。
- 有序性: 編譯器和處理器會對指令進行重排序寥茫,語句的順序發(fā)生改變,這樣在多線程的情況下险耀,可能出現(xiàn)奇怪的異常玖喘。
- 原子性: 只有對基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作累奈。
要解決這三個問題有兩種方式:
- volatile關鍵字:它只能解決兩個問題可見性和有序性問題澎媒,但是如果volatile修飾基本數(shù)據(jù)類型變量,而且這個變量只做讀取和賦值操作请敦,那么也沒有原子性問題了冬三。比如說用它來修飾boolean的變量勾笆。
- 加鎖:可以保證同一時間只有同一線程操作共享變量桥滨,當前線程操作共享變量時齐媒,共享變量不會被別的線程修改喻括,所以可見性、有序性和原子性問題都得到解決望蜡。分為synchronized同步鎖和JUC框架下的Lock鎖脖律。
二. volatile關鍵字
看過volatile關鍵字底層實現(xiàn)就知道
我們使用volatile關鍵字修飾變量小泉,就相當于給這個變量添加加了內(nèi)存屏障。那么內(nèi)存屏障的作用是什么呢让簿?
- 它會讓本地內(nèi)存共享變量副本無效,即修改了這個共享變量,它會被強制刷新到主內(nèi)存界逛。讀取這個共享變量息拜,會強制從主內(nèi)存中讀取最新值净响。因此解決了可見性問題馋贤。
- 禁止指令重排序,即在程序中在volatile變量進行操作時配乓,在其之前的操作肯定已經(jīng)全部執(zhí)行了,而且結果已經(jīng)對后面的操作可見崎页,在其之后的操作肯定還沒有執(zhí)行飒焦。因此解決了有序性問題牺荠。
這個的具體解釋志电,大家請看《深入理解Java內(nèi)存模型》里面關于happens-before規(guī)則的講解蛔趴。
class VolatileFeaturesExample {
//使用volatile聲明一個基本數(shù)據(jù)類型變量vl
volatile long vl = 0L;
//對于單個volatile基本數(shù)據(jù)類型變量賦值
public void set(long l) {
vl = l;
}
//對于單個volatile基本數(shù)據(jù)類型變量的復合操作
public void getAndIncrement () {
vl++;
}
//對于單個volatile基本數(shù)據(jù)類型變量讀取
public long get() {
return vl;
}
}
class VolatileFeaturesExample {
//聲明一個基本數(shù)據(jù)類型變量vl
long vl = 0L;
// 相當于加了同步鎖
public synchronized void set(long l) {
vl = l;
}
// 普通方法
public void getAndIncrement () {
long temp = get();
temp += 1L;
set(temp);
}
// 相當于加了同步鎖
public synchronized long get() {
return vl;
}
}
如果volatile修飾基本數(shù)據(jù)類型變量鱼蝉,而且只對這個變量做讀取和賦值操作,那么就相當于加了同步鎖渔隶。
三. synchronized同步鎖
synchronized同步鎖作用是訪問被鎖住的資源時间唉,只要獲取鎖的線程才能操作被鎖住的資源呈野,其他線程必須阻塞等待印叁。
所以一個線程來說轮蜕,可以阻塞等待跃洛,可以運行税课,那么線程到底有哪些狀態(tài)呢韩玩?
3.1 線程狀態(tài)
在Thread類中,有一個枚舉對象State標志著所有的線程狀態(tài)合愈。
// 標志線程狀態(tài)的枚舉對象
public enum State {
/**
* 新建狀態(tài)佛析。當創(chuàng)建一個線程Thread對象寸莫,但是還沒有調(diào)用它的start方法膘茎,就是這個狀態(tài)。
*/
NEW,
/**
* 運行狀態(tài)态坦。當前線程正在運行中
*/
RUNNABLE,
/**
* 阻塞狀態(tài)伞梯。
* 一般是鎖資源被另一線程持有谜诫,當前線程處于阻塞等待獲取鎖的狀態(tài)猜绣,
* 當線程獲取了鎖,并獲取CPU執(zhí)行權牺陶,就會從BLOCKED狀態(tài)轉成RUNNABLE狀態(tài)掰伸。
*
*/
BLOCKED,
/**
* 等待狀態(tài)狮鸭。調(diào)用三個方法當前線程會進人這個狀態(tài):
* 1. Object#wait() 方法
* 2. #join() 方法 (這個方法在Thread對象中歧蕉,本質(zhì)上也是調(diào)用wait()方法)
* 3. LockSupport#park() 方法
* 這三個方法調(diào)用時都沒有傳遞時間參數(shù)惯退,所以沒有超時限制催跪。
* WAITING狀態(tài)的線程是處于線程等待池中懊蒸,只有調(diào)用對應的喚醒方法,才能將當前線程從線程等待池中喚醒,
* 否則線程一直等待舌仍。除非發(fā)生中斷請求抡笼,也會將線程喚醒推姻。
* 喚醒線程的方法有:
* 1. Object#notify() notifyAll()
* 2. LockSupport#unpark()
* 注意join()是線程對象的wait()方法實現(xiàn)的藏古,當線程執(zhí)行完畢時拧晕,會調(diào)用自己的notifyAll()方法厂捞,
* 喚醒等待池中所有的線程靡馁。
*
* 還有要注意的是Object#wait() 方法只能在synchronized代碼塊中調(diào)用,
* 所以當線程被喚醒時赔嚎,它并不是處于可運行狀態(tài)尤误,而是處于BLOCKED狀態(tài)损晤,
* 因為只有獲取鎖的線程沉馆,才能執(zhí)行synchronized代碼塊中的代碼斥黑,所以被喚醒的線程要等待鎖锌奴。
*
* 而LockSupport#park()沒有這個方面的限制
*
*/
WAITING,
/**
* 等待超時狀態(tài)鹿蜀,調(diào)用下面五個方法當前線程會進人這個狀態(tài):
* 1. Object#wait(long)
* 2. #join(long) Thread.join茴恰,就是使用wait方法實現(xiàn)的往枣。
* 3. LockSupport#parkNanos
* 4. LockSupport#parkUntil
* 5. Thread#sleep
*
* 與WAITING狀態(tài)相比較分冈,當線程處于線程等待池中雕沉,如果沒有調(diào)用對應的喚醒方法,
* 但是超出規(guī)定時間扰路,那么線程自動會被喚醒幼衰。所以就是多出了一種喚醒方式。
* 注意Thread#sleep 沒有對應的喚醒方法肥印。
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
// 終止狀態(tài)深碱,當線程運行完畢時敷硅,就處于這個狀態(tài)绞蹦,而且該狀態(tài)不能再轉換成其他狀態(tài)幽七。
TERMINATED;
}
線程一共有六種狀態(tài):
- NEW: 新建狀態(tài)澡屡。當創(chuàng)建一個線程Thread對象驶鹉,但是還沒有調(diào)用它的start方法室埋,就是這個狀態(tài)词顾。
- RUNNABLE: 運行狀態(tài)肉盹。當前線程正在運行中上忍。
- BLOCKED: 阻塞狀態(tài)窍蓝。當前線程正在等待鎖資源吓笙。
- WAITING: 等待狀態(tài)面睛。當前線程處于線程等待池中叁鉴,需要被喚醒幌墓。
- TIMED_WAITING: 等待超時狀態(tài)常侣。與WAITING狀態(tài)相比袭祟,多了一種超時會被自動喚醒的方法。
- TERMINATED: 終止狀態(tài)鸟召,當線程運行完畢時欧募,就處于這個狀態(tài)跟继,而且該狀態(tài)不能再轉換成其他狀態(tài)舔糖。
注意處于等待狀態(tài)的線程只有兩種方式被喚醒:
- 調(diào)用對應的喚醒方法金吗。
- 調(diào)用該線程變量的interrupt()方法摇庙,會喚醒該線程卫袒,并拋出InterruptedException異常夕凝。
3.2 synchronized同步方法或者同步塊
synchronized同步方法或者同步塊具體是怎樣操作的呢?
- 相當于有一個大房間,房間門上有一把鎖lock膜蠢,房間里面存放的是所有與這把鎖lock關聯(lián)的同步方法或者同步塊挑围。
- 當某一個線程要執(zhí)行這把鎖lock的一個同步方法或者同步塊時杉辙,它就來到房間門前蜘矢,如果發(fā)現(xiàn)鎖lock還在品腹,那么它就拿著鎖進入房間舞吭,并將房間鎖上羡鸥,它可以執(zhí)行房間中任何一個同步方法或者同步塊存和。
- 這時又有另一個線程要執(zhí)行這把鎖lock的一個同步方法或者同步塊時哑姚,它就來到房間門前,發(fā)現(xiàn)鎖lock沒有了芜茵,就只能在門外等待叙量,此時該線程就在synchronized同步阻塞線程池中。
- 等到拿到鎖lock的線程九串,同步方法或者同步塊代碼執(zhí)行完畢绞佩,它就會從房間中退出來,將鎖放到門上猪钮。
- 這時在門外等待的線程就爭奪這把鎖lock,拿到鎖的線程就可以進入房間烤低,其他線程則又要繼續(xù)等待肘交。
注:synchronized 鎖是鎖住所有與這個鎖關聯(lián)的同步方法或者同步塊。
synchronized的同步鎖到底是什么呢扑馁?
其實就是java對象涯呻,在Java中,每一個對象都擁有一個鎖標記(monitor)腻要,也稱為監(jiān)視器复罐,多線程同時訪問某個對象時,線程只有獲取了該對象的鎖才能訪問雄家。
3.3 wait與notify效诅、notifyAll
這三個方法主要用于實現(xiàn)線程之間相互等待的問題。
調(diào)用對象lock的wait方法,會讓當前線程進行等待乱投,即將當前線程放入對象lock的線程等待池中咽笼。調(diào)用對象lock的notify方法會從線程等待池中隨機喚醒一個線程,notifyAll方法會喚醒所有線程篡腌。
注:對象lock的wait與notify褐荷、notifyAll方法調(diào)用必須放在以對象lock為鎖的同步方法或者同步塊中,否則會拋出IllegalMonitorStateException異常嘹悼。
wait與notify叛甫、notifyAll具體是怎么操作的呢?
- 前面過程與synchronized中介紹的一樣杨伙,當調(diào)用鎖lock的wait方法時其监,該線程(即當前線程)退出房間,歸還鎖lock限匣,但并不是進入synchronized同步阻塞線程池中抖苦,而是進入鎖lock的線程等待池中。
- 這時另一個線程拿到鎖lock進行房間米死,如果它執(zhí)行了鎖lock的notify方法锌历,那么就會從鎖lock的線程等待池中隨機喚醒一個線程,將它放入synchronized同步阻塞線程池中(記住只有拿到鎖lock的線程才能進行房間)峦筒。調(diào)用鎖lock的notifyAll方法,即喚醒線程等待池所有線程卤材。
使用wait與notify尉辑、notifyAll方法時,有兩點需要注意:
- wait與notify末贾、notifyAll方法這三個方法必須在synchronized同步代碼塊中執(zhí)行辉川,否則拋出IllegalMonitorStateException異常屿愚。
- 所以當我們使用notify娱据、notifyAll方法喚醒等待的線程時结啼,該線程不能立即執(zhí)行糕珊,因為它在synchronized同步代碼塊中,所以必須獲取鎖坟乾,才能繼續(xù)執(zhí)行印荔。
四. 其他重要方法
4.1 join方法
讓當前線程等待另一個線程執(zhí)行完成后实柠,才繼續(xù)執(zhí)行水泉。
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis) throws InterruptedException {
// 獲取當前系統(tǒng)毫秒數(shù)
long base = System.currentTimeMillis();
long now = 0;
// millis小于0,拋出異常
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
// 通過isAlive判斷當前線程是否存活
while (isAlive()) {
// wait(0)表示當前線程無限等待
wait(0);
}
} else {
// 通過isAlive判斷當前線程是否存活
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
// 當前線程等待delay毫秒窒盐,超過時間草则,當前線程就被喚醒
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
join方法是Thread中的方法,synchronized方法同步的鎖對象就是Thread對象登钥,通過調(diào)用Thread對象的wait方法畔师,讓當前線程等待
注意:這里是讓當前線程等待,即當前調(diào)用join方法的線程牧牢,而不是Thread對象的線程看锉。那么當前線程什么時候會被喚醒呢?
當Thread對象線程執(zhí)行完畢塔鳍,進入死亡狀態(tài)時伯铣,會調(diào)用Thread對象的notifyAll方法,來喚醒Thread對象的線程等待池中所有線程轮纫。
示例:
public static void joinTest() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 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方法
只是讓當前線程等待一定的時間腔寡,然后繼續(xù)執(zhí)行。
4.3 yield方法
將當前線程狀態(tài)從運行狀態(tài)轉成可運行狀態(tài)掌唾,如果再獲取CPU執(zhí)行權放前,還會繼續(xù)執(zhí)行。
4.4 interrupt方法
它會中斷處于WAITING和TIMED_WAITING狀態(tài)下的線程糯彬,而對其他狀態(tài)下的線程不起任何作用凭语。
示例:
public static void interruptTest() {
// 處于TIMED_WAITING狀態(tài)下的線程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+" 開始");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" 結束");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+" 產(chǎn)生異常");
}
}
}, "t1");
thread.start();
// 處于運行狀態(tài)下的線程
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 開始");
int i = 0;
while(i < Integer.MAX_VALUE - 10) {
i = i + 1;
for (int j = 0; j < i; j++);
}
System.out.println(Thread.currentThread().getName()+" i=="+i);
System.out.println(Thread.currentThread().getName()+" 結束");
}
}, "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方法
返回這個線程的中斷標志位。注意當調(diào)用線程的interrupt方法后撩扒,該線程的isInterrupted的方法就會返回true似扔。如果異常被處理了,又會將該標志位置位false搓谆,即isInterrupted的方法返回false炒辉。
4.6 線程優(yōu)先級以及守護線程
在java中線程優(yōu)先級范圍是1~10,默認的優(yōu)先級是5泉手。
在java中線程分為用戶線程和守護線程黔寇,isDaemon返回是true,表示它是守護線程斩萌。當所有的用戶線程執(zhí)行完畢后缝裤,java虛擬機就會退出状囱,不管是否還有守護線程未執(zhí)行完畢。
當創(chuàng)建一個新線程時倘是,這個新線程的優(yōu)先級等于創(chuàng)建它線程的優(yōu)先級,且只有當創(chuàng)建它線程是守護線程時袭艺,新線程才是守護線程搀崭。
當然也可以通過setPriority方法修改線程的優(yōu)先級,已經(jīng)setDaemon方法設置線程是否為守護線程猾编。
五. synchronized同步鎖與lock鎖
synchronized同步鎖與lock獨占鎖都可以保證并發(fā)操作安全問題瘤睹,即保證同一時間只有獲取鎖的那個線程才可以運行,其他線程必須等待答倡。
關于lock獨占鎖請閱讀我的AQS詳細介紹和ReentrantLock詳細分析相關文章轰传。
那么它們有什么異同點呢?
5.1 獲取鎖的方式不同
- 對于synchronized同步鎖:進入synchronized代碼塊中的線程瘪撇,會自動獲取鎖获茬,而其他線程就只能阻塞等待。
- 對于lock鎖:想要獲取lock鎖倔既,必須調(diào)用lock的lock系列方法恕曲,根據(jù)方法不同獲取鎖的方式也不同。
// 獲取鎖渤涌,如果獲取不到佩谣,就一直等待。不響應中斷請求
void lock();
// 獲取鎖实蓬,如果獲取不到茸俭,就一直等待。如果在線程等待期間有中斷請求就拋出異常
void lockInterruptibly() throws InterruptedException;
// 嘗試獲取鎖安皱,立即返回调鬓。返回true表示獲取成功,返回false表示獲取失敗
boolean tryLock();
// 在規(guī)定的unit時間內(nèi)獲取鎖练俐,如果時間到了還沒有獲取到鎖袖迎,則返回false,表示獲取失敗
// 如果在線程等待期間有中斷請求就拋出異常
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
5.2 釋放鎖的方式不同
- 當synchronized代碼塊執(zhí)行完成腺晾,或者拋出異常返回燕锥,都會自動釋放鎖,不需要用戶手動釋放悯蝉。
- 如果執(zhí)行完成归形,用戶必須主動調(diào)用unlock來釋放鎖,否則等待鎖的線程就會一直阻塞鼻由。為了防止發(fā)生異常暇榴,導致unlock方法沒有執(zhí)行厚棵,所以這個方法必須放在finally的代碼塊中。
5.3 等待鎖的線程狀態(tài)不一樣
- 等待synchronized同步鎖線程的狀態(tài)是 BLOCKED(阻塞狀態(tài))蔼紧。
- 等待lock鎖線程的狀態(tài)是WAITING(等待狀態(tài))或TIMED_WAITING(等待超時狀態(tài))婆硬。
記得我們在線程狀態(tài)中介紹過,處于WAITING與TIMED_WAITING狀態(tài)的線程奸例,是可以響應線程中斷的彬犯。而處于BLOCKED狀態(tài)的線程則不可以。
如果獲取synchronized鎖的線程一直不釋放鎖查吊,那么等待鎖的線程只能一直等待谐区,而獲取lock鎖的線程一直不釋放鎖,我們可以調(diào)用等待鎖的線程的interrupt()方法逻卖,將這個線程喚醒宋列。
其實Lock鎖中線程等待和喚醒主要是通過LockSupport類實現(xiàn)的,關于LockSupport請看JUC鎖框架_ LockSupport詳細分析這篇文章评也。
六. 實例講解
6.1 不加任何同步鎖
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
class Data {
int num;
public Data(int num) {
this.num = num;
}
public int getAndDecrement() {
return num--;
}
}
class MyRun implements Runnable {
private Data data;
// 用來記錄所有賣出票的編號
private List<Integer> list;
private CountDownLatch latch;
public MyRun(Data data, List<Integer> list, CountDownLatch latch) {
this.data = data;
this.list = list;
this.latch = latch;
}
@Override
public void run() {
try {
action();
} finally {
// 釋放latch共享鎖
latch.countDown();
}
}
// 進行買票操作炼杖,注意這里沒有使用data.num>0作為判斷條件,直到賣完線程退出仇参。
// 那么做會導致這兩處使用了共享變量data.num嘹叫,那么做多線程同步時,就要考慮更多條件诈乒。
// 這里只for循環(huán)了5次罩扇,表示每個線程只賣5張票,并將所有賣出去編號存入list集合中怕磨。
public void action() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
int newNum = data.getAndDecrement();
System.out.println("線程"+Thread.currentThread().getName()+" num=="+newNum);
list.add(newNum);
}
}
}
public class ThreadTest {
public static void startThread(Data data, String name, List<Integer> list,CountDownLatch latch) {
Thread t = new Thread(new MyRun(data, list, latch), name);
t.start();
}
public static void main(String[] args) {
// 使用CountDownLatch來讓主線程等待子線程都執(zhí)行完畢時喂饥,才結束
CountDownLatch latch = new CountDownLatch(6);
long start = System.currentTimeMillis();
// 這里用并發(fā)list集合
List<Integer> list = new CopyOnWriteArrayList();
Data data = new Data(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集合,進行排序和翻轉
Collections.sort(list);
Collections.reverse(list);
System.out.println(list);
long time = System.currentTimeMillis() - start;
// 輸出一共花費的時間
System.out.println("\n主線程結束 time=="+time);
}
}
輸出的結果是
線程t2 num==29
線程t6 num==27
線程t5 num==28
線程t4 num==28
線程t1 num==30
線程t3 num==30
線程t2 num==26
線程t4 num==24
線程t6 num==25
線程t5 num==23
線程t1 num==22
線程t3 num==21
線程t4 num==20
線程t6 num==19
線程t5 num==18
線程t2 num==17
線程t1 num==16
線程t3 num==15
線程t4 num==14
線程t5 num==12
線程t6 num==13
線程t1 num==9
線程t3 num==10
線程t2 num==11
線程t1 num==8
線程t6 num==5
線程t2 num==7
線程t5 num==3
線程t3 num==4
線程t4 num==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]
主線程結束 time==62
從結果中發(fā)現(xiàn)問題肠鲫,出現(xiàn)了重復票员帮,所以30張票沒有被賣完。最主要的原因就是Data類的getAndDecrement方法操作不是多線程安全的导饲。
- 首先它不能保證原子性捞高,分為三個操作,先讀取num的值渣锦,然后num自減硝岗,在返回自減前的值。
- 因為num不是volatile關鍵字修飾的袋毙,它也不能保證可見性和有序性型檀。
所以只要保證getAndDecrement方法多線程安全,那么就可以解決上面出現(xiàn)的問題听盖。那么保證getAndDecrement方法多線程安全呢胀溺?最簡單的方式就是在getAndDecrement方法前加synchronized關鍵字裂七。
這是synchronized關鍵鎖就是這個data對象實例,所以保證了多線程調(diào)用getAndDecrement方法時仓坞,只有一個線程能調(diào)用背零,等待調(diào)用完成,其他線程才能調(diào)用getAndDecrement方法无埃。
因為同一時間只有一個線程調(diào)用getAndDecrement方法捉兴,所以它在做num--操作時,不用擔心num變量會發(fā)生改變录语。所以原子性、可見性和有序性都可以得到保證禾乘。
6.2 使用最小同步鎖
class Data {
int num;
public Data(int num) {
this.num = num;
}
// 將getAndDecrement方法加了同步鎖
public synchronized int getAndDecrement() {
return num--;
}
}
輸出結果
線程t1 num==30
線程t2 num==29
線程t6 num==28
線程t4 num==26
線程t3 num==27
線程t5 num==25
線程t6 num==22
線程t2 num==21
線程t3 num==23
線程t1 num==24
線程t4 num==20
線程t5 num==19
線程t2 num==18
線程t3 num==17
線程t5 num==13
線程t4 num==14
線程t6 num==16
線程t1 num==15
線程t2 num==12
線程t4 num==9
線程t1 num==7
線程t5 num==10
線程t3 num==11
線程t6 num==8
線程t4 num==6
線程t2 num==3
線程t1 num==2
線程t3 num==4
線程t5 num==5
線程t6 num==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]
主線程結束 time==61
我們只是將Data的getAndDecrement方法加了同步鎖澎埠,發(fā)現(xiàn)解決了多線程并發(fā)問題。主要是因為我們只在一處使用了共享變量num始藕,所以只需要將這處加同步就行了蒲稳。而且你會發(fā)現(xiàn)最后花費的總時間與沒加同步鎖時幾乎一樣,那么因為我們同步代碼足夠小伍派。
相反地江耀,我們加地同步鎖不合理,可能也能實現(xiàn)多線程安全诉植,但是耗時就會大大增加祥国。
6.3 不合理地使用同步鎖
@Override
public void run() {
try {
synchronized (data){
action();
}
} finally {
// 釋放latch共享鎖
latch.countDown();
}
}
輸入結果:
線程t1 num==30
線程t1 num==29
線程t1 num==28
線程t1 num==27
線程t1 num==26
線程t6 num==25
線程t6 num==24
線程t6 num==23
線程t6 num==22
線程t6 num==21
線程t5 num==20
線程t5 num==19
線程t5 num==18
線程t5 num==17
線程t5 num==16
線程t4 num==15
線程t4 num==14
線程t4 num==13
線程t4 num==12
線程t4 num==11
線程t3 num==10
線程t3 num==9
線程t3 num==8
線程t3 num==7
線程t3 num==6
線程t2 num==5
線程t2 num==4
線程t2 num==3
線程t2 num==2
線程t2 num==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]
主線程結束 time==342
在這里我們將整個action方法,放入同步代碼塊中晾腔,也可以解決多線程沖突問題舌稀,但是所耗費的時間是在getAndDecrement方法上加同步鎖時間的幾倍。
所以我們在加同步鎖的時候灼擂,那些需要同步壁查,就是看那些地方使用了共享變量。比如這里只在getAndDecrement方法中使用了同步變量剔应,所以只要給它加鎖就行了睡腿。
但是如果在action方法中,使用data.num>0來作為循環(huán)條件峻贮,那么在加同步鎖時席怪,就必須將整個action方法放在同步模塊中,因為我們必須保證月洛,在data.num>0判斷到getAndDecrement方法調(diào)用這些代碼都是在同步模塊中何恶,不然就會產(chǎn)生多線程沖突問題。