?????在前面我們介紹的一些內(nèi)容中锈死,我們的程序都是一條執(zhí)行流,一步一步的執(zhí)行穆壕。但其實(shí)這種程序?qū)ξ覀冇?jì)算機(jī)的資源的使用上是低效的待牵。例如:我們有一個(gè)用于計(jì)算的程序,主程序計(jì)算數(shù)據(jù)喇勋,在計(jì)算的過(guò)程中每得到一個(gè)結(jié)果就需要將其保存到外部磁盤上缨该,那么難道我們的主程序每次都要停止等待CPU將結(jié)果保存到磁盤之后,再繼續(xù)完成計(jì)算工作嗎茄蚯?要知道磁盤的速度可是巨慢的(相對(duì)內(nèi)存而言)压彭,我們?nèi)绻芊忠粋€(gè)線程去完成磁盤的寫(xiě)入工作睦优,主線程還是繼續(xù)計(jì)算的話,是不是效率更高了呢壮不?其實(shí)汗盘,并發(fā)就是這樣的一種思想,使用時(shí)間片分發(fā)給各個(gè)線程CPU的使用時(shí)間询一,給人感覺(jué)好像程序在同時(shí)做多個(gè)事情一樣隐孽,這樣做的好處主要在于它能夠?qū)ξ覀冋麄€(gè)的計(jì)算機(jī)資源有一個(gè)充分的利用,在多個(gè)線程競(jìng)爭(zhēng)計(jì)算機(jī)資源不沖突的前提下健蕊,充分的利用我們的資源菱阵。本篇文章首先來(lái)介紹并發(fā)的最基本的內(nèi)容-----線程。主要涉及以下一些內(nèi)容:
- 定義線程的兩種不同的方法及它們之間的區(qū)別
- 線程的幾種不同的狀態(tài)及其區(qū)別
- Thread類中的一些線程屬性和方法
- 多線程遇到的幾個(gè)典型的問(wèn)題
?????一缩功、創(chuàng)建一個(gè)線程
?????首先我們看創(chuàng)建一個(gè)線程的第一種方式晴及,繼承Thread類并重寫(xiě)其run方法。
public class MyThread extends Thread {
@Override
public void run(){
System.out.println("this is mythread");
}
}
現(xiàn)在我們來(lái)看看在主程序中如何啟動(dòng)我們自定義的線程:
public static void main(String[] args) {
Thread myThread = new MyThread();
myThread.start();
}
我們首先構(gòu)建一個(gè)Thread實(shí)例嫡锌,調(diào)用其start方法虑稼,調(diào)用該方法會(huì)為線程分配其所必須的堆棧資源,計(jì)數(shù)器势木,時(shí)間片等蛛倦,并在該方法的結(jié)束時(shí)刻調(diào)用我們重寫(xiě)的run方法,完成線程的啟動(dòng)啦桌。
但是在Java中類是單繼承的溯壶,也就是如果某個(gè)類已經(jīng)有了父類,那么它就不能被定義成線程類甫男。當(dāng)然且改,Java中也提供了第二種方法來(lái)定義一個(gè)線程類,這種方式實(shí)際上更加的接近本質(zhì)一些查剖。通過(guò)繼承接口Runnable并在其內(nèi)部重寫(xiě)一個(gè)run方法钾虐。
public class MyThread implements Runnable{
@Override
public void run(){
System.out.println("this is mythread");
}
}
啟動(dòng)線程的方式和上一種略微有點(diǎn)不同噪窘,但是本質(zhì)上都是一樣的笋庄。
public static void main(String[] args) {
Thread myThread = new Thread(new MyThread());
myThread.start();
}
這里我們利用Thread的一個(gè)構(gòu)造函數(shù),傳入一個(gè)實(shí)現(xiàn)了Runnable接口的參數(shù)倔监。下面我們看看這個(gè)構(gòu)造函數(shù)的具體實(shí)現(xiàn):
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
調(diào)用init方法對(duì)線程的一些狀態(tài)優(yōu)先級(jí)等做一個(gè)初始化的操作直砂,我們順便看看使用第一種方式創(chuàng)建線程實(shí)例的那個(gè)無(wú)參的構(gòu)造函數(shù):
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
可以看到,兩個(gè)構(gòu)造函數(shù)的內(nèi)部調(diào)用的是同一個(gè)方法浩习,只是傳入的參數(shù)不同而已静暂。所以他們之間的區(qū)別就在于初始化的時(shí)候這個(gè)Runnable參數(shù)是否為空,當(dāng)然這個(gè)參數(shù)的用處在run方法中也可以看出來(lái):
@Override
public void run() {
if (target != null) {
target.run();
}
}
如果我們使用第二種方式構(gòu)建Thread實(shí)例谱秽,那么此處的target肯定不會(huì)是null洽蛀,自然會(huì)調(diào)用我們重寫(xiě)的run方法摹迷。如果使用的是第一種方式構(gòu)建的Thread實(shí)例,那么就不會(huì)調(diào)用上述的run方法郊供,而是調(diào)用的我們重寫(xiě)的Thread的run方法峡碉,所以從本質(zhì)上看,兩種方式的底層處理都是一樣的驮审。
這就是創(chuàng)建一個(gè)線程類并啟動(dòng)該線程的兩種不同的方式鲫寄,表面上略有不同,但是實(shí)際上都是一樣的調(diào)用init方法完成初始化疯淫。對(duì)于啟動(dòng)線程的start方法的源碼地来,由于調(diào)用本地native方法,暫時(shí)并不易解釋熙掺,有興趣的可以使用jvm指令查看本地方法的實(shí)現(xiàn)以了解整個(gè)線程從分配資源到調(diào)用run方法啟動(dòng)的全過(guò)程未斑。
?????二、線程的多種狀態(tài)
?????線程是有狀態(tài)的币绩,它會(huì)因?yàn)榈貌坏芥i而阻塞處于BLOCKED狀態(tài)颂碧,會(huì)因?yàn)闂l件不足而等待處于WAITING狀態(tài)等。Thread中有一個(gè)枚舉類型囊括了所有的線程狀態(tài):
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
NEW狀態(tài)表示線程剛剛被定義类浪,還未實(shí)際獲得資源以啟動(dòng)载城,也就是還未調(diào)用start方法。
RUNNABLE表示線程當(dāng)前處于運(yùn)行狀態(tài)费就,當(dāng)然也有可能由于時(shí)間片使用完了而等待CPU重新的調(diào)度诉瓦。
BLOCKED表示線程在競(jìng)爭(zhēng)某個(gè)鎖失敗時(shí)被置于阻塞狀態(tài)
WAITING和TIMED_WAITING表示線程在運(yùn)行中由于缺少某個(gè)條件而不得不被置于條件等待隊(duì)列等待需要的條件或資源。
TERMINATED表示線程運(yùn)行結(jié)束力细,當(dāng)線程的run方法結(jié)束之后睬澡,該線程就會(huì)是TERMINATED狀態(tài)。
我們可以調(diào)用Thread的getState方法返回當(dāng)前線程的狀態(tài):
/*定義一個(gè)線程類*/
public class MyThread implements Runnable{
@Override
public void run(){
System.out.println("myThread's state is : "+Thread.currentThread().getState());
}
}
/*啟動(dòng)線程*/
public static void main(String[] args) throws InterruptedException {
Thread myThread = new Thread(new MyThread());
myThread.start();
Thread.sleep(1000);
System.out.println("myThread's state is : "+myThread.getState());
}
我們兩次輸出myThread線程的當(dāng)前狀態(tài)眠蚂,在run方法中輸出結(jié)果顯示該線程狀態(tài)為RUNNABLE煞聪,當(dāng)該run方法執(zhí)行結(jié)束時(shí)候,我們又一次輸出該線程的當(dāng)前狀態(tài)逝慧,結(jié)果顯示該線程處于TERMINATED昔脯。至于更加復(fù)雜的線程狀態(tài),我們將在后續(xù)的文章中逐漸進(jìn)行介紹笛臣。
?????三云稚、Thread類中的其他一些常用屬性及方法
?????以上我們介紹了創(chuàng)建線程的兩種不同的方式以及線程的幾種不同狀態(tài),有關(guān)于線程信息屬性的一些方法還沒(méi)有介紹沈堡。本小節(jié)將來(lái)簡(jiǎn)單介紹下線程所具有的基本的一些屬性以及一些常用的方法静陈。
首先每個(gè)線程都有一個(gè)id和一個(gè)name屬性,id是一個(gè)遞增的整數(shù),每創(chuàng)建一個(gè)線程該id就會(huì)加一拐格,該id的初始值是10,每創(chuàng)建一個(gè)線程就會(huì)往上加一刑赶。所以該id也間接的告訴了我們當(dāng)前線程在所有線程中的位置禁荒。name屬性往往是以“Thread-”+編號(hào)作為某個(gè)具體線程的name值。例如:
public static void main(String[] args){
for (int i=0;i<10;i++){
Thread myThread = new Thread(new MyThread());
myThread.start();
System.out.println(myThread.getName());
}
}
輸出結(jié)果:
除此之外角撞,Thread中還有一個(gè)屬性daemon呛伴,它是一個(gè)boolean類型的變量,該變量指示了當(dāng)前線程是否是一個(gè)守護(hù)線程谒所。守護(hù)線程主要用于輔助主線程完成工作热康,如果主線程執(zhí)行結(jié)束,那么它的守護(hù)線程也會(huì)跟著結(jié)束劣领。例如:我們的main程序在執(zhí)行的時(shí)候姐军,始終有一個(gè)垃圾回收線程作為守護(hù)線程輔助一些對(duì)象的回收工作,當(dāng)main程序執(zhí)行結(jié)束時(shí)尖淘,守護(hù)線程也將退出內(nèi)存奕锌。關(guān)于守護(hù)線程有幾個(gè)方法:
public final boolean isDaemon() :判斷當(dāng)前線程是否是守護(hù)線程
public final void setDaemon(boolean on):設(shè)置當(dāng)前線程是否作為守護(hù)線程
還有一個(gè)方法較為常見(jiàn),join村生。該方法可以讓一個(gè)線程等待另一個(gè)線程執(zhí)行結(jié)束之后再繼續(xù)工作惊暴。例如:
public class MyThread implements Runnable{
@Override
public void run(){
System.out.println("myThread is running");
}
}
public static void main(String[] args) {
Thread myThread = new Thread(new MyThread());
myThread.start();
//主線程等待myThread線程執(zhí)行結(jié)束
myThread.join();
System.out.println("waiting myThread done....");
}
輸出結(jié)果:
有人可能會(huì)疑問(wèn),我們使用多線程不就是為了充分利用計(jì)算機(jī)資源趁桃,使其同時(shí)執(zhí)行多個(gè)任務(wù)辽话,為什么又要讓一個(gè)線程等待另一個(gè)線程呢?其實(shí)某些時(shí)候卫病,主線程需要拿到所有分支線程計(jì)算的結(jié)果再一次進(jìn)行計(jì)算油啤,各個(gè)分支線程的進(jìn)度各有快慢,主線程唯有等待他們?nèi)繄?zhí)行結(jié)束之后才能繼續(xù)蟀苛。此時(shí)就需要使用join方法了益咬,所以說(shuō)每一個(gè)方法的存在都有其可應(yīng)用的場(chǎng)景。至于這個(gè)join的源代碼也是很有研究?jī)r(jià)值的帜平,我們將在后續(xù)的文章中對(duì)其源代碼的實(shí)現(xiàn)進(jìn)行進(jìn)一步的學(xué)習(xí)幽告。
還有一些屬性和方法,限于篇幅罕模,本文不再繼續(xù)學(xué)習(xí)评腺,大家可以自行查看源碼進(jìn)行學(xué)習(xí)帘瞭。下面我們看看多線程之后可能會(huì)遇到的幾個(gè)經(jīng)典的問(wèn)題淑掌。
?????四、多線程遇到的幾個(gè)典型的問(wèn)題
?????第一個(gè)可能遇到的問(wèn)題是蝶念,競(jìng)態(tài)條件抛腕。也就是說(shuō)芋绸,當(dāng)多個(gè)線程同時(shí)訪問(wèn)操作同一個(gè)對(duì)象的時(shí)候,最終的結(jié)果可能正確也可能不正確担敌,具體的執(zhí)行情況和線程實(shí)際的執(zhí)行時(shí)序有關(guān)摔敛。
例如:
/*我們定義一個(gè)線程*/
public class MyThread implements Runnable{
public static int count;
@Override
public void run(){
try {
Thread.currentThread().sleep((int)(Math.random()*100));
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/*main方法中啟動(dòng)多個(gè)線程*/
public static void main(String[] args){
Thread[] threads = new Thread[100];
for (int i=0;i<100;i++){
threads[i] = new Thread(new MyThread());
threads[i].start();
}
for (int j =0;j<100;j++){
threads[j].join();
}
System.out.println(MyThread.count);
}
首先在我們自定義的線程類中,有一個(gè)static公共變量全封,而我們的run方法主要就做兩個(gè)事情马昙,隨機(jī)睡一會(huì)和count增一。再來(lái)看main函數(shù)刹悴,首先定義了一百個(gè)線程并逐個(gè)啟動(dòng)行楞,然后主線程等待所有的子線程完成之后輸出count的值。
按照我們一般的思維土匀,這一百個(gè)線程子房,每個(gè)線程都是為count加一,最后的輸出結(jié)果應(yīng)該是100才對(duì)就轧。但是實(shí)際上我們多次運(yùn)行該程序得到的結(jié)果都是不一樣的证杭,但幾乎都是小于100的。
為什么會(huì)出現(xiàn)這樣的情況呢妒御?主要原因還是在于為count加一這個(gè)操作解愤,它并非是原子操作,也就是說(shuō)想要為count加一需要經(jīng)過(guò)起碼兩個(gè)步驟:
- 取count的當(dāng)前值
- 為count加一
因?yàn)槊總€(gè)線程都是隨機(jī)睡了一會(huì)乎莉,有可能兩個(gè)線程同時(shí)醒來(lái)琢歇,都獲取到當(dāng)前的count的值,又同時(shí)為其加一梦鉴,這樣就導(dǎo)致兩個(gè)不同的線程卻只為count增加了一次值李茫。這種情況在多線程的前提下,發(fā)生的概率就更大了肥橙,所以這也是為什么我們得到的結(jié)果始終小于100但又每次都不同的原因魄宏。
第二個(gè)問(wèn)題是,內(nèi)存的可見(jiàn)性問(wèn)題存筏。就是說(shuō)宠互,如果兩個(gè)線程共享了同一個(gè)參數(shù),其中一個(gè)線程對(duì)共享參數(shù)的修改而另一個(gè)線程并不會(huì)立馬能夠看到椭坚。原因是這些修改會(huì)被暫存在CPU緩存中予跌,而沒(méi)有立馬寫(xiě)回內(nèi)存。例如:
public class MyThread extends Thread{
public static boolean flag = false;
@Override
public void run(){
while(!flag){
//just running
}
System.out.println("my thread has finished ");
}
}
public static void main(String[] args) throws InterruptedException {
Thread myThread = new MyThread();
myThread.start();
Thread.sleep(1000);
MyThread.flag = true;
System.out.println("main thread has finished");
}
首先我們定義一個(gè)線程類善茎,該線程類中有一個(gè)靜態(tài)共享變量flag券册,run方法做的事情很簡(jiǎn)單,死循環(huán)的做一些事情,等待外部線程更改flag的值烁焙,使其退出循環(huán)航邢。而main方法首先啟動(dòng)一個(gè)線程,然后修改共享變量flag的值骄蝇,按照常理線程myThread在main線程修改flag變量的值之后將退出循環(huán)膳殷,打印退出信息。但是實(shí)際的輸出結(jié)果為:
main線程已經(jīng)結(jié)束了九火,而整個(gè)程序并沒(méi)有結(jié)束赚窃,線程myThread的結(jié)束信息也沒(méi)有被打印,這就說(shuō)明myThread線程還困在while循環(huán)中岔激,但是實(shí)際上主線程已經(jīng)將flag的值修改了考榨,只是myThread無(wú)法看見(jiàn)。這是什么原因呢鹦倚?
我們知道河质,每個(gè)線程都有一些緩存,往往為了效率震叙,對(duì)一個(gè)變量值的修改并不會(huì)立馬寫(xiě)會(huì)內(nèi)存掀鹅,而是注入緩存中,等到一定的時(shí)候才寫(xiě)回內(nèi)存媒楼,而當(dāng)別的線程來(lái)修改這些共享的變量的時(shí)候乐尊,他們是從內(nèi)存進(jìn)行讀取的,修改后可能也沒(méi)有及時(shí)的寫(xiě)回內(nèi)存中划址,這就很容易導(dǎo)致其他線程根本就看不到你所做的修改扔嵌。這就是典型的內(nèi)存可見(jiàn)性問(wèn)題。
本小節(jié)簡(jiǎn)單的介紹了多線程的兩個(gè)典型的問(wèn)題夺颤,解決辦法其實(shí)有多種痢缎,我們將在下篇文章中涉及。
以上的本篇內(nèi)容主要介紹了線程的基本概念世澜,如何創(chuàng)建一個(gè)線程独旷,如何啟動(dòng)一個(gè)線程,還有與線程相關(guān)的一些基本的屬性和方法寥裂,總結(jié)不到之處嵌洼,望大家指出,相互學(xué)習(xí)封恰。下篇文章將介紹一個(gè)用于解決多線程并發(fā)問(wèn)題的關(guān)鍵字synchronized麻养。