一篇學(xué)會(huì)Java多線程之線程
本文轉(zhuǎn)載自微信公眾號(hào)「我是開發(fā)者FTD」,作者FTD。轉(zhuǎn)載本文請(qǐng)聯(lián)系我是開發(fā)者FTD公眾號(hào)。
我們?cè)趯W(xué)習(xí)軟件開發(fā)時(shí),多線程,高并發(fā)是一個(gè)必不可少的知識(shí)點(diǎn),也是在面試時(shí)必會(huì)問到的內(nèi)容,為了讓大家對(duì)多線程,高并發(fā)編程有個(gè)清晰認(rèn)識(shí),特地組織了一個(gè)專欄來(lái)專門介紹一下,希望能對(duì)大家有一些幫助。
線程簡(jiǎn)介
線程是程序運(yùn)行的基本執(zhí)行單元。當(dāng)操作系統(tǒng)(不包括單線程的操作系統(tǒng),如微軟早期的DOS)在執(zhí)行一個(gè)程序時(shí),會(huì)在系統(tǒng)中建立一個(gè)進(jìn)程,而在這個(gè)進(jìn)程中,必須至少建立一個(gè)線程(這個(gè)線程被稱為主線程)來(lái)作為這個(gè)程序運(yùn)行的入口點(diǎn)。因此,在操作系統(tǒng)中運(yùn)行的任何程序都至少有一個(gè)主線程。
進(jìn)程和線程是現(xiàn)代操作系統(tǒng)中兩個(gè)必不可少的運(yùn)行模型。在操作系統(tǒng)中可以有多個(gè)進(jìn)程,這些進(jìn)程包括系統(tǒng)進(jìn)程(由操作系統(tǒng)內(nèi)部建立的進(jìn)程)和用戶進(jìn)程(由用戶程序建立的進(jìn)程);一個(gè)進(jìn)程中可以有一個(gè)或多個(gè)線程。進(jìn)程和進(jìn)程之間不共享內(nèi)存,也就是說系統(tǒng)中的進(jìn)程是在各自獨(dú)立的內(nèi)存空間中運(yùn)行的。而一個(gè)進(jìn)程中的線可以共享系統(tǒng)分派給這個(gè)進(jìn)程的內(nèi)存空間。
在進(jìn)一步介紹之前,我們先來(lái)了解一些基本概念,以幫助大家更快速的理解。
基本概念
進(jìn)程
進(jìn)程是操作系統(tǒng)中正在執(zhí)行的不同的應(yīng)用程序,例如:我們可以同時(shí)打開微信和QQ,甚至更多的程序。
在操作系統(tǒng)中運(yùn)行的程序就是進(jìn)程,進(jìn)程就是執(zhí)行程序的一次執(zhí)行過程,它是一個(gè)動(dòng)態(tài)的概念式系統(tǒng)資源分配的單位
通常在一個(gè)進(jìn)程中可以包含若干個(gè)線程,當(dāng)然一個(gè)進(jìn)程中至少有一個(gè)線程,不然沒有存在的意義,線程是CPU調(diào)度和執(zhí)行的單位
線程
線程是一個(gè)應(yīng)用程序進(jìn)程中不同的執(zhí)行路徑,例如:我們的WEB服務(wù)器,能夠?yàn)槎鄠€(gè)用戶同時(shí)提供請(qǐng)求服務(wù)。
進(jìn)程是不活潑的,進(jìn)程從來(lái)不執(zhí)行任何東西,它只是線程的容器。線程總是在某個(gè)進(jìn)程環(huán)境中創(chuàng)建的,而且它的整個(gè)生命周期都在該進(jìn)程中。
在一個(gè)進(jìn)程中,如果創(chuàng)建了多個(gè)線程,線程的運(yùn)行是由調(diào)度器安排調(diào)度的,調(diào)度器是與操作系統(tǒng)緊密相關(guān)的,先后順序是不能人為干預(yù)的
多線程
多線程擁有多條執(zhí)行路徑,「主線程與子線程并行交替執(zhí)行」(普通方法只有主線程一條路徑),對(duì)同一份資源操作時(shí),會(huì)存在資源搶奪的問題,這時(shí)就需要加入并發(fā)控制了。
一個(gè)Java應(yīng)用程序,至少有三個(gè)線程: main()主線程, gc()垃圾回收線程,異常處理線程。當(dāng)然如果發(fā)生異常,會(huì)影響主線程。
多線程程序的優(yōu)點(diǎn):
- 提高應(yīng)用程序的響應(yīng)。對(duì)圖形化界面更有意義,可增強(qiáng)用戶體驗(yàn)。同時(shí)做多個(gè)事情。比如:一邊聽歌、一邊寫代碼。
- 提高計(jì)算機(jī)系統(tǒng)CPU的利用率。不過線程也會(huì)帶來(lái)額外的開銷,如CPU調(diào)度時(shí)間,并發(fā)控制帶來(lái)的系統(tǒng)開銷。
- 改善程序結(jié)構(gòu)。將既長(zhǎng)又復(fù)雜的進(jìn)程分為多個(gè)線程,獨(dú)立運(yùn)行,利于理解和修改。
何時(shí)需要多線程?
程序需要同時(shí)執(zhí)行兩個(gè)或多個(gè)任務(wù)。
需要一些后臺(tái)運(yùn)行的程序時(shí),比如:Java后臺(tái)運(yùn)行的GC線程。
創(chuàng)建線程
Java中創(chuàng)建線程有四種方式,我們下面依次介紹一下。
1、繼承 Thread 類
(1)定義Thread類的子類,并重寫該類的run方法,該run方法的方法體就代表了線程要完成的任務(wù)。因此把run()方法稱為執(zhí)行體。
(2)創(chuàng)建Thread子類的實(shí)例對(duì)象,即創(chuàng)建了一個(gè)線程對(duì)象。
(3)調(diào)用該線程對(duì)象的start()方法來(lái)啟動(dòng)該線程。
示例代碼:
- public class MyThread extends Thread {
- // 總票數(shù)
- public int count = 10;
- @Override
- public void run() {
- // 當(dāng)還有票時(shí)就繼續(xù)售賣
- while (count > 0) {
- // 剩余票數(shù)
- count--;
- System.out.println(
- Thread.currentThread().getName() + "售賣第 " + (10 - count) + " 張票,當(dāng)前剩余票數(shù): " + count);
- }
- }
- public static void main(String[] args) {
- MyThread myThread = new MyThread();
- myThread.start();
- }
- }
2、實(shí)現(xiàn)Runnable接口
(1)定義Runnable接口的實(shí)現(xiàn)類,并重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執(zhí)行體。
(2)創(chuàng)建 Runnable實(shí)現(xiàn)類的實(shí)例對(duì)象,并將該實(shí)例作為Thread的target來(lái)創(chuàng)建一個(gè)Thread對(duì)象,該Thread對(duì)象才是真正的線程運(yùn)行對(duì)象。
(3)調(diào)用該線程對(duì)象的start()方法來(lái)啟動(dòng)線程。
示例代碼:
- public class MyRunableThread implements Runnable {
- // 總票數(shù)
- public int count = 10;
- @Override
- public void run() {
- // 當(dāng)還有票時(shí)就繼續(xù)售賣
- while (count > 0) {
- // 剩余票數(shù)
- count--;
- System.out.println(
- Thread.currentThread().getName() + "售賣第 " + (10 - count) + " 張票,當(dāng)前剩余票數(shù): " + count);
- }
- }
- public static void main(String[] args) {
- MyRunableThread myRunableThread = new MyRunableThread();
- Thread myThread = new Thread(myRunableThread);
- myThread.start();
- }
- }
「Thread 和 Runnable 的區(qū)別」
上述兩種方法是大家最常見到的兩種創(chuàng)建線程的方法,也常常會(huì)被問到兩種方式創(chuàng)建線程的區(qū)別,下面簡(jiǎn)單總結(jié)了一下:
「繼承 Thread 類」
子類繼承 Thread 類具備多線程能力
啟動(dòng)線程:子類線程對(duì)象調(diào)用 .start()方法
不建議使用:避免 OOP 單繼承局限性
「實(shí)現(xiàn)接口 Runnable」
- 具有多線程能力
- 啟動(dòng)線程:傳入目標(biāo)對(duì)象 + Thread對(duì)象調(diào)用.start()方法
- 推薦使用:避免單繼承局限性,方便同一個(gè)對(duì)象被多個(gè)線程使用
另外,在使用線程池時(shí)只能放入實(shí)現(xiàn)Runable或Callable類線程,不能直接放入繼承Thread的類
3、實(shí)現(xiàn)Callable接口
(1)創(chuàng)建Callable接口的實(shí)現(xiàn)類,并實(shí)現(xiàn)call()方法,該call()方法將作為線程執(zhí)行體,并且有返回值。
(2)創(chuàng)建Callable實(shí)現(xiàn)類的實(shí)例,使用FutureTask類來(lái)包裝Callable對(duì)象,該FutureTask對(duì)象封裝了該Callable對(duì)象的call()方法的返回值。
(3)使用FutureTask對(duì)象作為Thread對(duì)象的target創(chuàng)建并啟動(dòng)新線程。
(4)調(diào)用FutureTask對(duì)象的get()方法來(lái)獲得子線程執(zhí)行結(jié)束后的返回值。
示例代碼:
- public class MyCallableThread implements Callable<String> {
- // 總票數(shù)
- private int count = 10;
- @Override
- public String call() throws Exception {
- // 當(dāng)還有票時(shí)就繼續(xù)售賣
- while (count > 0) {
- // 剩余票數(shù)
- count--;
- System.out.println(
- Thread.currentThread().getName() + "售賣第 " + (10 - count) + " 張票,當(dāng)前剩余票數(shù): " + count);
- }
- return "票已售完";
- }
- public static void main(String[] args) throws InterruptedException, ExecutionException {
- Callable<String> callable = new MyCallableThread();
- FutureTask<String> futureTask = new FutureTask<>(callable);
- Thread myThread = new Thread(futureTask);
- myThread.start();
- // 打印返回結(jié)果
- System.out.println(futureTask.get());
- }
- }
「Runnable和Callable的區(qū)別:」
Callable規(guī)定的方法是call(),Runnable規(guī)定的方法是run()。
Callable的任務(wù)執(zhí)行后可返回值,而Runnable的任務(wù)是不能有返回值。
call方法可以拋出異常,run方法不可以。
4、線程池
Java默認(rèn)提供了五種線程池,通過Executors創(chuàng)建,分別為:
- 「newCachedThreadPool」 創(chuàng)建一個(gè)可緩存線程池,如果線程池長(zhǎng)度超過處理需要,可靈活回收空閑線程,若無(wú)可回收,則新建線程。
- 「newFixedThreadPool」 創(chuàng)建一個(gè)定長(zhǎng)線程池,可控制線程最大并發(fā)數(shù),超出的線程會(huì)在隊(duì)列中等待。
- 「newScheduledThreadPool」 創(chuàng)建一個(gè)定長(zhǎng)線程池,支持定時(shí)及周期性任務(wù)執(zhí)行。
- 「newSingleThreadExecutor」 創(chuàng)建一個(gè)單線程化的線程池,它只會(huì)用唯一的工作線程來(lái)執(zhí)行任務(wù),保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級(jí))執(zhí)行。
- 「newWorkStealingPool」 創(chuàng)建一個(gè)具有搶占式操作的線程池,由于能夠合理的使用CPU進(jìn)行對(duì)任務(wù)操作(并行操作),所以適合使用在很耗時(shí)的任務(wù)中。
示例代碼:
- public class MyThreadPool implements Runnable {
- // 總票數(shù)
- public int count = 10;
- @Override
- public void run() {
- // 當(dāng)還有票時(shí)就繼續(xù)售賣
- while (count > 0) {
- // 剩余票數(shù)
- count--;
- System.out.println(
- Thread.currentThread().getName() + "售賣第 " + (10 - count) + " 張票,當(dāng)前剩余票數(shù): " + count);
- }
- }
- public static void main(String[] args) {
- ExecutorService ex = Executors.newFixedThreadPool(5);
- MyThreadPool t = new MyThreadPool();
- ex.submit(t);
- ex.shutdown();
- }
- }
關(guān)于線程池,后續(xù)會(huì)有單獨(dú)文章給大家詳細(xì)介紹。
通過以上的內(nèi)容,希望大家可以對(duì)線程有個(gè)初步的認(rèn)識(shí),相關(guān)示例代碼,稍后整理后我會(huì)上傳到GitHub上,也請(qǐng)大家留意我們的后續(xù)文章。