?????在前面我們介紹的一些內(nèi)容中,我們的程序都是一條執(zhí)行流,一步一步的執(zhí)行。但其實這種程序?qū)ξ覀冇嬎銠C的資源的使用上是低效的。例如:我們有一個用于計算的程序,主程序計算數(shù)據(jù),在計算的過程中每得到一個結(jié)果就需要將其保存到外部磁盤上,那么難道我們的主程序每次都要停止等待CPU將結(jié)果保存到磁盤之后,再繼續(xù)完成計算工作嗎?要知道磁盤的速度可是巨慢的(相對內(nèi)存而言),我們?nèi)绻芊忠粋€線程去完成磁盤的寫入工作,主線程還是繼續(xù)計算的話,是不是效率更高了呢?其實,并發(fā)就是這樣的一種思想,使用時間片分發(fā)給各個線程CPU的使用時間,給人感覺好像程序在同時做多個事情一樣,這樣做的好處主要在于它能夠?qū)ξ覀冋麄€的計算機資源有一個充分的利用,在多個線程競爭計算機資源不沖突的前提下,充分的利用我們的資源。本篇文章首先來介紹并發(fā)的最基本的內(nèi)容-----線程。主要涉及以下一些內(nèi)容:
- 定義線程的兩種不同的方法及它們之間的區(qū)別
- 線程的幾種不同的狀態(tài)及其區(qū)別
- Thread類中的一些線程屬性和方法
- 多線程遇到的幾個典型的問題
?????一、創(chuàng)建一個線程
?????首先我們看創(chuàng)建一個線程的第一種方式,繼承Thread類并重寫其run方法。
public class MyThread extends Thread {
@Override
public void run(){
System.out.println("this is mythread");
}
}
現(xiàn)在我們來看看在主程序中如何啟動我們自定義的線程:
public static void main(String[] args) {
Thread myThread = new MyThread();
myThread.start();
}
我們首先構(gòu)建一個Thread實例,調(diào)用其start方法,調(diào)用該方法會為線程分配其所必須的堆棧資源,計數(shù)器,時間片等,并在該方法的結(jié)束時刻調(diào)用我們重寫的run方法,完成線程的啟動。
但是在Java中類是單繼承的,也就是如果某個類已經(jīng)有了父類,那么它就不能被定義成線程類。當(dāng)然,Java中也提供了第二種方法來定義一個線程類,這種方式實際上更加的接近本質(zhì)一些。通過繼承接口Runnable并在其內(nèi)部重寫一個run方法。
public class MyThread implements Runnable{
@Override
public void run(){
System.out.println("this is mythread");
}
}
啟動線程的方式和上一種略微有點不同,但是本質(zhì)上都是一樣的。
public static void main(String[] args) {
Thread myThread = new Thread(new MyThread());
myThread.start();
}
這里我們利用Thread的一個構(gòu)造函數(shù),傳入一個實現(xiàn)了Runnable接口的參數(shù)。下面我們看看這個構(gòu)造函數(shù)的具體實現(xiàn):
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
調(diào)用init方法對線程的一些狀態(tài)優(yōu)先級等做一個初始化的操作,我們順便看看使用第一種方式創(chuàng)建線程實例的那個無參的構(gòu)造函數(shù):
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
可以看到,兩個構(gòu)造函數(shù)的內(nèi)部調(diào)用的是同一個方法,只是傳入的參數(shù)不同而已。所以他們之間的區(qū)別就在于初始化的時候這個Runnable參數(shù)是否為空,當(dāng)然這個參數(shù)的用處在run方法中也可以看出來:
@Override
public void run() {
if (target != null) {
target.run();
}
}
如果我們使用第二種方式構(gòu)建Thread實例,那么此處的target肯定不會是null,自然會調(diào)用我們重寫的run方法。如果使用的是第一種方式構(gòu)建的Thread實例,那么就不會調(diào)用上述的run方法,而是調(diào)用的我們重寫的Thread的run方法,所以從本質(zhì)上看,兩種方式的底層處理都是一樣的。
這就是創(chuàng)建一個線程類并啟動該線程的兩種不同的方式,表面上略有不同,但是實際上都是一樣的調(diào)用init方法完成初始化。對于啟動線程的start方法的源碼,由于調(diào)用本地native方法,暫時并不易解釋,有興趣的可以使用jvm指令查看本地方法的實現(xiàn)以了解整個線程從分配資源到調(diào)用run方法啟動的全過程。
?????二、線程的多種狀態(tài)
?????線程是有狀態(tài)的,它會因為得不到鎖而阻塞處于BLOCKED狀態(tài),會因為條件不足而等待處于WAITING狀態(tài)等。Thread中有一個枚舉類型囊括了所有的線程狀態(tài):
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
NEW狀態(tài)表示線程剛剛被定義,還未實際獲得資源以啟動,也就是還未調(diào)用start方法。
RUNNABLE表示線程當(dāng)前處于運行狀態(tài),當(dāng)然也有可能由于時間片使用完了而等待CPU重新的調(diào)度。
BLOCKED表示線程在競爭某個鎖失敗時被置于阻塞狀態(tài)
WAITING和TIMED_WAITING表示線程在運行中由于缺少某個條件而不得不被置于條件等待隊列等待需要的條件或資源。
TERMINATED表示線程運行結(jié)束,當(dāng)線程的run方法結(jié)束之后,該線程就會是TERMINATED狀態(tài)。
我們可以調(diào)用Thread的getState方法返回當(dāng)前線程的狀態(tài):
/*定義一個線程類*/
public class MyThread implements Runnable{
@Override
public void run(){
System.out.println("myThread's state is : "+Thread.currentThread().getState());
}
}
/*啟動線程*/
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é)束時候,我們又一次輸出該線程的當(dāng)前狀態(tài),結(jié)果顯示該線程處于TERMINATED。至于更加復(fù)雜的線程狀態(tài),我們將在后續(xù)的文章中逐漸進行介紹。
?????三、Thread類中的其他一些常用屬性及方法
?????以上我們介紹了創(chuàng)建線程的兩種不同的方式以及線程的幾種不同狀態(tài),有關(guān)于線程信息屬性的一些方法還沒有介紹。本小節(jié)將來簡單介紹下線程所具有的基本的一些屬性以及一些常用的方法。
首先每個線程都有一個id和一個name屬性,id是一個遞增的整數(shù),每創(chuàng)建一個線程該id就會加一,該id的初始值是10,每創(chuàng)建一個線程就會往上加一。所以該id也間接的告訴了我們當(dāng)前線程在所有線程中的位置。name屬性往往是以“Thread-”+編號作為某個具體線程的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中還有一個屬性daemon,它是一個boolean類型的變量,該變量指示了當(dāng)前線程是否是一個守護線程。守護線程主要用于輔助主線程完成工作,如果主線程執(zhí)行結(jié)束,那么它的守護線程也會跟著結(jié)束。例如:我們的main程序在執(zhí)行的時候,始終有一個垃圾回收線程作為守護線程輔助一些對象的回收工作,當(dāng)main程序執(zhí)行結(jié)束時,守護線程也將退出內(nèi)存。關(guān)于守護線程有幾個方法:
public final boolean isDaemon() :判斷當(dāng)前線程是否是守護線程
public final void setDaemon(boolean on):設(shè)置當(dāng)前線程是否作為守護線程
還有一個方法較為常見,join。該方法可以讓一個線程等待另一個線程執(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é)果:
有人可能會疑問,我們使用多線程不就是為了充分利用計算機資源,使其同時執(zhí)行多個任務(wù),為什么又要讓一個線程等待另一個線程呢?其實某些時候,主線程需要拿到所有分支線程計算的結(jié)果再一次進行計算,各個分支線程的進度各有快慢,主線程唯有等待他們?nèi)繄?zhí)行結(jié)束之后才能繼續(xù)。此時就需要使用join方法了,所以說每一個方法的存在都有其可應(yīng)用的場景。至于這個join的源代碼也是很有研究價值的,我們將在后續(xù)的文章中對其源代碼的實現(xiàn)進行進一步的學(xué)習(xí)。
還有一些屬性和方法,限于篇幅,本文不再繼續(xù)學(xué)習(xí),大家可以自行查看源碼進行學(xué)習(xí)。下面我們看看多線程之后可能會遇到的幾個經(jīng)典的問題。
?????四、多線程遇到的幾個典型的問題
?????第一個可能遇到的問題是,競態(tài)條件。也就是說,當(dāng)多個線程同時訪問操作同一個對象的時候,最終的結(jié)果可能正確也可能不正確,具體的執(zhí)行情況和線程實際的執(zhí)行時序有關(guān)。
例如:
/*我們定義一個線程*/
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方法中啟動多個線程*/
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);
}
首先在我們自定義的線程類中,有一個static公共變量,而我們的run方法主要就做兩個事情,隨機睡一會和count增一。再來看main函數(shù),首先定義了一百個線程并逐個啟動,然后主線程等待所有的子線程完成之后輸出count的值。
按照我們一般的思維,這一百個線程,每個線程都是為count加一,最后的輸出結(jié)果應(yīng)該是100才對。但是實際上我們多次運行該程序得到的結(jié)果都是不一樣的,但幾乎都是小于100的。
為什么會出現(xiàn)這樣的情況呢?主要原因還是在于為count加一這個操作,它并非是原子操作,也就是說想要為count加一需要經(jīng)過起碼兩個步驟:
- 取count的當(dāng)前值
- 為count加一
因為每個線程都是隨機睡了一會,有可能兩個線程同時醒來,都獲取到當(dāng)前的count的值,又同時為其加一,這樣就導(dǎo)致兩個不同的線程卻只為count增加了一次值。這種情況在多線程的前提下,發(fā)生的概率就更大了,所以這也是為什么我們得到的結(jié)果始終小于100但又每次都不同的原因。
第二個問題是,內(nèi)存的可見性問題。就是說,如果兩個線程共享了同一個參數(shù),其中一個線程對共享參數(shù)的修改而另一個線程并不會立馬能夠看到。原因是這些修改會被暫存在CPU緩存中,而沒有立馬寫回內(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");
}
首先我們定義一個線程類,該線程類中有一個靜態(tài)共享變量flag,run方法做的事情很簡單,死循環(huán)的做一些事情,等待外部線程更改flag的值,使其退出循環(huán)。而main方法首先啟動一個線程,然后修改共享變量flag的值,按照常理線程myThread在main線程修改flag變量的值之后將退出循環(huán),打印退出信息。但是實際的輸出結(jié)果為:
main線程已經(jīng)結(jié)束了,而整個程序并沒有結(jié)束,線程myThread的結(jié)束信息也沒有被打印,這就說明myThread線程還困在while循環(huán)中,但是實際上主線程已經(jīng)將flag的值修改了,只是myThread無法看見。這是什么原因呢?
我們知道,每個線程都有一些緩存,往往為了效率,對一個變量值的修改并不會立馬寫會內(nèi)存,而是注入緩存中,等到一定的時候才寫回內(nèi)存,而當(dāng)別的線程來修改這些共享的變量的時候,他們是從內(nèi)存進行讀取的,修改后可能也沒有及時的寫回內(nèi)存中,這就很容易導(dǎo)致其他線程根本就看不到你所做的修改。這就是典型的內(nèi)存可見性問題。
本小節(jié)簡單的介紹了多線程的兩個典型的問題,解決辦法其實有多種,我們將在下篇文章中涉及。
以上的本篇內(nèi)容主要介紹了線程的基本概念,如何創(chuàng)建一個線程,如何啟動一個線程,還有與線程相關(guān)的一些基本的屬性和方法,總結(jié)不到之處,望大家指出,相互學(xué)習(xí)。下篇文章將介紹一個用于解決多線程并發(fā)問題的關(guān)鍵字synchronized。