java的多線程用法編程總結(jié)
一、進(jìn)程與線程
1、進(jìn)程是什么?
狹義定義:進(jìn)程是正在運(yùn)行的程序的實(shí)例(an instance of a computer program that is being executed)。
廣義定義:進(jìn)程是一個(gè)具有一定獨(dú)立功能的程序關(guān)于某個(gè)數(shù)據(jù)集合的一次運(yùn)行活動(dòng)。它是操作系統(tǒng)動(dòng)態(tài)執(zhí)行的基本單元,在傳統(tǒng)的操作系統(tǒng)中,進(jìn)程既是基本的分配單元,也是基本的執(zhí)行單元。
2、線程是什么?
線程,有時(shí)被稱為輕量級(jí)進(jìn)程(Lightweight Process,LWP),是程序執(zhí)行流的最小單元。一個(gè)標(biāo)準(zhǔn)的線程由線程ID,當(dāng)前指令指針(PC),寄存器集合和堆棧組成。另外,線程是進(jìn)程中的一個(gè)實(shí)體,是被系統(tǒng)獨(dú)立調(diào)度和分派的基本單位,線程自己不擁有系統(tǒng)資源,只擁有一點(diǎn)兒在運(yùn)行中必不可少的資源,但它可與同屬一個(gè)進(jìn)程的其它線程共享進(jìn)程所擁有的全部資源。
3、進(jìn)程和線程的區(qū)別?
進(jìn)程和線程的主要差別在于它們是不同的操作系統(tǒng)資源管理方式。
進(jìn)程有獨(dú)立的地址空間,一個(gè)進(jìn)程崩潰后,在保護(hù)模式下不會(huì)對(duì)其它進(jìn)程產(chǎn)生影響,而線程只是一個(gè)進(jìn)程中的不同執(zhí)行路徑。
線程有自己的堆棧和局部變量,但線程之間沒有單獨(dú)的地址空間,一個(gè)線程死掉就等于整個(gè)進(jìn)程死掉,所以多進(jìn)程的程序要比多線程的程序健壯,但在進(jìn)程切換時(shí),耗費(fèi)資源較大,效率要差一些。但對(duì)于一些要求同時(shí)進(jìn)行并且又要共享某些變量的并發(fā)操作,只能用線程,不能用進(jìn)程。
簡言之,線程與進(jìn)程的區(qū)別就是:
(1)一個(gè)程序至少有一個(gè)進(jìn)程,一個(gè)進(jìn)程至少有一個(gè)線程;
(2) 線程的劃分尺度小于進(jìn)程,使得多線程程序的并發(fā)性高。
(3)進(jìn)程在執(zhí)行過程中擁有獨(dú)立的內(nèi)存單元,而多個(gè)線程共享內(nèi)存,從而極大地提高了程序的運(yùn)行效率。
(4)線程在執(zhí)行過程中與進(jìn)程是有區(qū)別的。每個(gè)獨(dú)立的線程有一個(gè)程序運(yùn)行的入口、順序執(zhí)行序列和程序的出口。但是線程不能夠獨(dú)立執(zhí)行,必須依存在應(yīng)用程序中,由應(yīng)用程序提供多個(gè)線程執(zhí)行控制。
(5)從邏輯角度來看,多線程的意義在于一個(gè)應(yīng)用程序中,有多個(gè)執(zhí)行部分可以同時(shí)執(zhí)行。但操作系統(tǒng)并沒有將多個(gè)線程看做多個(gè)獨(dú)立的應(yīng)用,來實(shí)現(xiàn)進(jìn)程的調(diào)度和管理以及資源分配。
這就是進(jìn)程和線程的重要區(qū)別。
二、線程的生命周期及五種基本狀態(tài)
Java線程具有五種基本狀態(tài):
(1)新建狀態(tài)(New):當(dāng)線程對(duì)象對(duì)創(chuàng)建后,即進(jìn)入了新建狀態(tài),如:Thread t = new MyThread();
(2)就緒狀態(tài)(Runnable):當(dāng)調(diào)用線程對(duì)象的start()方法(t.start();),線程即進(jìn)入就緒狀態(tài)。處于就緒狀態(tài)的線程,只是說明此線程已經(jīng)做好了準(zhǔn)備,隨時(shí)等待CPU調(diào)度執(zhí)行,并不是說執(zhí)行了t.start()此線程立即就會(huì)執(zhí)行;
(3)運(yùn)行狀態(tài)(Running):當(dāng)CPU開始調(diào)度處于就緒狀態(tài)的線程時(shí),此時(shí)線程才得以真正執(zhí)行,即進(jìn)入到運(yùn)行狀態(tài)。注:就 緒狀態(tài)是進(jìn)入到運(yùn)行狀態(tài)的唯一入口,也就是說,線程要想進(jìn)入運(yùn)行狀態(tài)執(zhí)行,首先必須處于就緒狀態(tài)中;
(4)阻塞狀態(tài)(Blocked):處于運(yùn)行狀態(tài)中的線程由于某種原因,暫時(shí)放棄對(duì)CPU的使用權(quán),停止執(zhí)行,此時(shí)進(jìn)入阻塞狀態(tài),直到其進(jìn)入到就緒狀態(tài),才 有機(jī)會(huì)再次被CPU調(diào)用以進(jìn)入到運(yùn)行狀態(tài)。根據(jù)阻塞產(chǎn)生的原因不同,阻塞狀態(tài)又可以分為三種:
①等待阻塞:運(yùn)行狀態(tài)中的線程執(zhí)行wait()方法,使本線程進(jìn)入到等待阻塞狀態(tài);
②同步阻塞:線程在獲取synchronized同步鎖失敗(因?yàn)殒i被其它線程所占用),它會(huì)進(jìn)入同步阻塞狀態(tài);
③其他阻塞 : 通過調(diào)用線程的sleep()或join()或發(fā)出了I/O請(qǐng)求時(shí),線程會(huì)進(jìn)入到阻塞狀態(tài)。當(dāng)sleep()狀態(tài)超時(shí)、join()等待線程終止或者超時(shí)、或者I/O處理完畢時(shí),線程重新轉(zhuǎn)入就緒狀態(tài)。
(5)死亡狀態(tài)(Dead):線程執(zhí)行完了或者因異常退出了run()方法,該線程結(jié)束生命周期。
三、Java多線程的實(shí)現(xiàn)
在Java中,如果要實(shí)現(xiàn)多線程的程序,那么必須依靠一個(gè)線程的主體類(好比主類的概念一樣,表示一個(gè)線程的主類),但是這個(gè)線程的主體類在定義的時(shí)候需要有一些特殊的要求,這個(gè)類可以繼承Thread類或?qū)崿F(xiàn)Runnable接口來完成定義。
1、繼承Thread類實(shí)現(xiàn)多線程
java.lang.Thread是一個(gè)負(fù)責(zé)線程操作的類,任何的類繼承了Thread類就可以成為一個(gè)線程的主類。既然是主類,必須有它的使用方法,而線程啟動(dòng)的主方法需要覆寫Thread類中的run()方法才可以。
定義一個(gè)線程的主體類:
class MyThread extends Thread { // 線程的主體類 private String title; public MyThread(String title) { this.title = title; } @Override public void run() { // 線程的主方法 for (int x = 0; x < 10; x++) { System.out.println(this.title + "運(yùn)行,x = " + x); } } }
現(xiàn)在已經(jīng)有了線程類,并且里面也存在了相應(yīng)的操作方法,那么就應(yīng)該產(chǎn)生對(duì)象并調(diào)用里面的方法,于是編寫出了下的程序:
public class TestDemo { public static void main(String[] args) { MyThread mt1 = new MyThread("線程A"); MyThread mt2 = new MyThread("線程B"); MyThread mt3 = new MyThread("線程C"); mt1.run(); mt2.run(); mt3.run(); }
運(yùn)行結(jié)果:
線程A運(yùn)行,x = 0
線程A運(yùn)行,x = 1
線程A運(yùn)行,x = 2
線程A運(yùn)行,x = 3
線程A運(yùn)行,x = 4
線程A運(yùn)行,x = 5
線程A運(yùn)行,x = 6
線程A運(yùn)行,x = 7
線程A運(yùn)行,x = 8
線程A運(yùn)行,x = 9
線程B運(yùn)行,x = 0
線程B運(yùn)行,x = 1
線程B運(yùn)行,x = 2
線程B運(yùn)行,x = 3
線程B運(yùn)行,x = 4
線程B運(yùn)行,x = 5
線程B運(yùn)行,x = 6
線程B運(yùn)行,x = 7
線程B運(yùn)行,x = 8
線程B運(yùn)行,x = 9
線程C運(yùn)行,x = 0
線程C運(yùn)行,x = 1
線程C運(yùn)行,x = 2
線程C運(yùn)行,x = 3
線程C運(yùn)行,x = 4
線程C運(yùn)行,x = 5
線程C運(yùn)行,x = 6
線程C運(yùn)行,x = 7
線程C運(yùn)行,x = 8
線程C運(yùn)行,x = 9
我們發(fā)現(xiàn):以上的操作并沒有真正的啟動(dòng)多線程,因?yàn)槎鄠€(gè)線程彼此之間的執(zhí)行一定是交替的方式運(yùn)行,而此時(shí)是順序執(zhí)行,每一個(gè)對(duì)象的代碼執(zhí)行完之后才向下繼續(xù)執(zhí)行。
如果要想在程序之中真正的啟動(dòng)多線程,必須依靠Thread類的一個(gè)方法:public void start(),表示真正啟動(dòng)多線程,調(diào)用此方法后會(huì)間接調(diào)用run()方法:
public class TestDemo { public static void main(String[] args) { MyThread mt1 = new MyThread("線程A"); MyThread mt2 = new MyThread("線程B"); MyThread mt3 = new MyThread("線程C"); mt1.start(); mt2.start(); mt3.start(); } }
運(yùn)行結(jié)果:
線程C運(yùn)行,x = 0
線程A運(yùn)行,x = 0
線程B運(yùn)行,x = 0
線程A運(yùn)行,x = 1
線程C運(yùn)行,x = 1
線程A運(yùn)行,x = 2
線程B運(yùn)行,x = 1
線程A運(yùn)行,x = 3
線程A運(yùn)行,x = 4
線程A運(yùn)行,x = 5
線程C運(yùn)行,x = 2
線程C運(yùn)行,x = 3
線程C運(yùn)行,x = 4
線程C運(yùn)行,x = 5
線程C運(yùn)行,x = 6
線程C運(yùn)行,x = 7
線程C運(yùn)行,x = 8
線程C運(yùn)行,x = 9
線程A運(yùn)行,x = 6
線程A運(yùn)行,x = 7
線程A運(yùn)行,x = 8
線程A運(yùn)行,x = 9
線程B運(yùn)行,x = 2
線程B運(yùn)行,x = 3
線程B運(yùn)行,x = 4
線程B運(yùn)行,x = 5
線程B運(yùn)行,x = 6
線程B運(yùn)行,x = 7
線程B運(yùn)行,x = 8
線程B運(yùn)行,x = 9
此時(shí)可以發(fā)現(xiàn):多個(gè)線程之間彼此交替執(zhí)行,但每次的執(zhí)行結(jié)果是不一樣的。通過以上的代碼就可以得出結(jié)論:要想啟動(dòng)線程必須依靠Thread類的start()方法執(zhí)行,線程啟動(dòng)之后會(huì)默認(rèn)調(diào)用了run()方法。
在調(diào)用start()方法之后,發(fā)生了一系列復(fù)雜的事情:
(1)啟動(dòng)新的執(zhí)行線程(具有新的調(diào)用棧);
(2)該線程從新狀態(tài)轉(zhuǎn)移到可運(yùn)行狀態(tài);
(3)當(dāng)該線程獲得機(jī)會(huì)執(zhí)行時(shí),其目標(biāo)run()方法將運(yùn)行。
注意:對(duì)Java來說,run()方法沒有任何特別之處。像main()方法一樣,它只是新線程知道調(diào)用的方法名稱(和簽名)。因此,在Runnable上或者Thread上調(diào)用run方法是合法的,但并不啟動(dòng)新的線程。
說明:為什么線程啟動(dòng)的時(shí)候必須調(diào)用start()而不是直接調(diào)用run()?
我們發(fā)現(xiàn),在調(diào)用了start()之后,實(shí)際上它執(zhí)行的還是覆寫后的run()方法,那為什么不直接調(diào)用run()方法呢?為了解釋此問題,下面打開Thread類的源代碼,觀察一下start()方法的定義:
public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } private native void start0();
打開此方法的源代碼首先可以發(fā)現(xiàn):方法會(huì)拋出一個(gè)“IllegalThreadStateException”異常。一般來講,如果一個(gè)方法中使用了throw拋出一個(gè)異常對(duì)象,那么這個(gè)異常應(yīng)該使用try…catch捕獲,或者是方法的聲明上使用throws拋出,但是這塊都沒有,為什么呢?因?yàn)檫@個(gè)異常類是屬于運(yùn)行時(shí)異常(RuntimeException)的子類:
java.lang.Object
|- java.lang.Throwable
|- java.lang.Exception
|- java.lang.RuntimeException
|- java.lang.IllegalArgumentException
|- java.lang.IllegalThreadStateException
當(dāng)一個(gè)線程對(duì)象被重復(fù)啟動(dòng)之后會(huì)拋出此異常,即:一個(gè)線程對(duì)象只能啟動(dòng)一次。
在start()方法之中有一個(gè)最為關(guān)鍵的部分就是start0()方法,而且這個(gè)方法上使用了一個(gè)native關(guān)鍵字的定義。
native關(guān)鍵字指的是Java本地接口調(diào)用(Java Native Interface),即:是使用Java調(diào)用本機(jī)操作系統(tǒng)的函數(shù)功能完成一些特殊的操作,而這樣的代碼開發(fā)在Java之中幾乎很少出現(xiàn),因?yàn)镴ava的最大特點(diǎn)是可移植性,如果一個(gè)程序只能在固定的操作系統(tǒng)上使用,那么可移植性就將徹底的喪失,所以,此操作一般不用。
多線程的實(shí)現(xiàn)一定需要操作系統(tǒng)的支持,那么以上的start0()方法實(shí)際上就和抽象方法很類似沒有方法體,而這個(gè)方法體交給JVM去實(shí)現(xiàn),即:在windows下的JVM可能使用A方法實(shí)現(xiàn)了start0(),而在Linux下的JVM可能使用了B方法實(shí)現(xiàn)了start0(),但是在調(diào)用的時(shí)候并不會(huì)去關(guān)心具體是何方式實(shí)現(xiàn)了start0()方法,只會(huì)關(guān)心最終的操作結(jié)果,交給JVM去匹配了不同的操作系統(tǒng)。
所以在多線程操作之中,使用start()方法啟動(dòng)多線程的操作是需要進(jìn)行操作系統(tǒng)函數(shù)調(diào)用的。
2、實(shí)現(xiàn)Runnable接口實(shí)現(xiàn)多線程
使用Thread類的確是可以方便的進(jìn)行多線程的實(shí)現(xiàn),但是這種方式最大的缺點(diǎn)就是單繼承的問題。為此,在java之中也可以利用Runnable接口來實(shí)現(xiàn)多線程。這個(gè)接口的定義如下:
public interface Runnable { public void run(); }
通過Runnable接口實(shí)現(xiàn)多線程:
class MyThread implements Runnable { // 線程的主體類 private String title; public MyThread(String title) { this.title = title; } @Override public void run() { // 線程的主方法 for (int x = 0; x < 10; x++) { System.out.println(this.title + "運(yùn)行,x = " + x); } } }
這和之前繼承Thread類的方式區(qū)別不大,但是有一個(gè)優(yōu)點(diǎn)就是避免了單繼承局限。
不過問題來了。之前說過,如果要啟動(dòng)多線程,需要依靠Thread類的start()方法完成,之前繼承Thread類的時(shí)候可以將此方法直接繼承過來使用,但現(xiàn)在實(shí)現(xiàn)的是Runable接口,沒有這個(gè)方法可以繼承了,怎么辦?
要解決這個(gè)問題,還是需要依靠Thread類完成。在Thread類中定義了一個(gè)構(gòu)造方法,接收Runnable接口對(duì)象:
public Thread(Runnable target);
利用Thread類啟動(dòng)多線程:
public class TestDemo { public static void main(String[] args) throws Exception { MyThread mt1 = new MyThread("線程A"); MyThread mt2 = new MyThread("線程B"); MyThread mt3 = new MyThread("線程C"); new Thread(mt1).start(); new Thread(mt2).start(); new Thread(mt3).start(); } }
運(yùn)行結(jié)果:
線程A運(yùn)行,x = 0
線程B運(yùn)行,x = 0
線程B運(yùn)行,x = 1
線程C運(yùn)行,x = 0
線程B運(yùn)行,x = 2
線程A運(yùn)行,x = 1
線程B運(yùn)行,x = 3
線程C運(yùn)行,x = 1
線程C運(yùn)行,x = 2
線程B運(yùn)行,x = 4
線程B運(yùn)行,x = 5
線程A運(yùn)行,x = 2
線程A運(yùn)行,x = 3
線程A運(yùn)行,x = 4
線程A運(yùn)行,x = 5
線程A運(yùn)行,x = 6
線程A運(yùn)行,x = 7
線程A運(yùn)行,x = 8
線程A運(yùn)行,x = 9
線程B運(yùn)行,x = 6
線程B運(yùn)行,x = 7
線程B運(yùn)行,x = 8
線程B運(yùn)行,x = 9
線程C運(yùn)行,x = 3
線程C運(yùn)行,x = 4
線程C運(yùn)行,x = 5
線程C運(yùn)行,x = 6
線程C運(yùn)行,x = 7
線程C運(yùn)行,x = 8
線程C運(yùn)行,x = 9
此時(shí),不但實(shí)現(xiàn)了多線程的啟動(dòng),而且沒有了單繼承局限。
3、實(shí)現(xiàn)多線程的第三種方法:.使用Callable接口實(shí)現(xiàn)多線程
使用Runnable接口實(shí)現(xiàn)的多線程可以避免單繼承局限,但是有一個(gè)問題,Runnable接口里面的run()方法不能返回操作結(jié)果。為了解決這個(gè)問題,提供了一個(gè)新的接口:Callable接口(java.util.concurrent.Callable)。
public interface Callable<V>{ public V call() throws Exception; }
執(zhí)行完Callable接口中的call()方法會(huì)返回一個(gè)結(jié)果,這個(gè)返回結(jié)果的類型由Callable接口上的泛型決定。
實(shí)現(xiàn)Callable接口來實(shí)現(xiàn)多線程的具體操作是:
創(chuàng)建Callable接口的實(shí)現(xiàn)類,并實(shí)現(xiàn)clall()方法;然后使用FutureTask類來包裝Callable實(shí)現(xiàn)類的對(duì)象,且以此FutureTask對(duì)象作為Thread對(duì)象的target來創(chuàng)建線程。
定義一個(gè)線程主體類:
import java.util.concurrent.Callable; class MyThread implements Callable<String>{ private int ticket = 10; @Override public String call() throws Exception { for(int i = 0 ; i < 20 ; i++){ if(this.ticket > 0){ System.out.println("賣票,剩余票數(shù)為"+ this.ticket --); } } return "票已賣光"; } }
Thread類沒有直接支持Callable接口。而在JDK1.5之后,提供了一個(gè)類:
java.util.concurrent.FutureTask<V>
這個(gè)類主要負(fù)責(zé)Callable接口對(duì)象操作。其定義結(jié)構(gòu)如下:
public class FutureTask<V>
extends Object
implements RunnableFurture<V>
而RunnableFurture這個(gè)接口又有如下定義:
public interface RunnableFurture<V>
extends Runnable,Future<V>
在FutureTask 類里面定義有如下構(gòu)造方法:
public FutureTask(Callable<V> callable)
現(xiàn)在,終于可以通過FutureTask類來接收Callable接口對(duì)象了,接收的目的是為了取得call()方法的返回結(jié)果。
從上面分析我們可以發(fā)現(xiàn):
FutureTask類可以接收Callable接口對(duì)象,而FutureTask類實(shí)現(xiàn)了RunnableFurture接口,RunnableFurture接口又繼承了Runnable接口。
于是,我們可以這樣來啟動(dòng)多線程:
public class TestDemo { public static void main(String[] args) throws Exception { MyThread mt1 = new MyThread(); MyThread mt2 = new MyThread(); FutureTask<String> task1 = new FutureTask<String>(mt1);//取得call()方法返回結(jié)果 FutureTask<String> task2 = new FutureTask<String>(mt2);//取得call()方法返回結(jié)果 //FutureTask是Runnable接口的子類,可以使用Thread類的構(gòu)造來接收task對(duì)象 new Thread(task1).start(); new Thread(task2).start(); //多線程執(zhí)行完畢后,可以使用FutureTask的父接口Future中的get()方法取得執(zhí)行結(jié)果 System.out.println("線程1的返回結(jié)果:"+task1.get()); System.out.println("線程2的返回結(jié)果:"+task2.get()); } }
運(yùn)行結(jié)果:
賣票,剩余票數(shù)為10
賣票,剩余票數(shù)為10
賣票,剩余票數(shù)為9
賣票,剩余票數(shù)為8
賣票,剩余票數(shù)為7
賣票,剩余票數(shù)為9
賣票,剩余票數(shù)為6
賣票,剩余票數(shù)為8
賣票,剩余票數(shù)為5
賣票,剩余票數(shù)為7
賣票,剩余票數(shù)為4
賣票,剩余票數(shù)為6
賣票,剩余票數(shù)為3
賣票,剩余票數(shù)為5
賣票,剩余票數(shù)為2
賣票,剩余票數(shù)為4
賣票,剩余票數(shù)為1
賣票,剩余票數(shù)為3
賣票,剩余票數(shù)為2
賣票,剩余票數(shù)為1
線程1的返回結(jié)果:票已賣光
線程2的返回結(jié)果:票已賣光
小結(jié):
上述講解了三種實(shí)現(xiàn)多線程的方式,對(duì)于線程的啟動(dòng)而言,都是調(diào)用線程對(duì)象的start()方法,需要特別注意的是:不能對(duì)同一線程對(duì)象兩次調(diào)用start()方法。
相關(guān)文章
Java8 HashMap鍵與Comparable接口小結(jié)
這篇文章主要介紹了Java8 HashMap鍵與Comparable接口小結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01maven如何打包動(dòng)態(tài)環(huán)境變量(包括啟動(dòng)腳本)
這篇文章主要介紹了maven如何打包動(dòng)態(tài)環(huán)境變量(包括啟動(dòng)腳本)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04springboot啟動(dòng)時(shí)如何獲取端口和項(xiàng)目名
這篇文章主要介紹了springboot啟動(dòng)時(shí)如何獲取端口和項(xiàng)目名,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11Java使用application.property讀取文件里面的值
本文通過實(shí)例代碼給大家介紹了Java使用application.property讀取文件里面的值,需要的朋友可以參考下2018-10-10