Java-多線程編程

前言:

Java的多線程編程隨著應(yīng)用功能越來越復(fù)雜,用戶對(duì)軟件體驗(yàn)要求越來越高,那么對(duì)于多線程的編程越發(fā)重要了。接下來先來了解一下多線程的一些概念。

1、CPU核心數(shù)和線程數(shù)的關(guān)系

多核心:也指單芯片多處理器( Chip Multiprocessors,簡稱CMP),CMP是由美國斯坦福大學(xué)提出的,其思想是將大規(guī)模并行處理器中的SMP(對(duì)稱多處理器)集成到同一芯片內(nèi),各個(gè)處理器并行執(zhí)行不同的進(jìn)程。這種依靠多個(gè)CPU同時(shí)并行地運(yùn)行程序是實(shí)現(xiàn)超高速計(jì)算的一個(gè)重要方向,稱為并行處理
多線程: Simultaneous Multithreading.簡稱SMT.SMT可通過復(fù)制處理器上的結(jié)構(gòu)狀態(tài),讓同一個(gè)處理器上的多個(gè)線程同步執(zhí)行并共享處理器的執(zhí)行資源可最大限度地實(shí)現(xiàn)寬發(fā)射、亂序的超標(biāo)量處理,提高處理器運(yùn)算部件的利用率,緩和由于數(shù)據(jù)相關(guān)或 Cache未命中帶來的訪問內(nèi)存延時(shí)。
核心數(shù)、線程數(shù):目前主流CPU有雙核、三核和四核,六核也在2010年發(fā)布。增加核心數(shù)目就是為了增加線程數(shù),因?yàn)椴僮飨到y(tǒng)是通過線程來執(zhí)行任務(wù)的,一般情況下它們是1:1對(duì)應(yīng)關(guān)系,也就是說四核CPU一般擁有四個(gè)線程。但 Intel引入超線程技術(shù)后,使核心數(shù)與線程數(shù)形成1:2的關(guān)系。

2、CPU時(shí)間片輪轉(zhuǎn)機(jī)制

我們平時(shí)在開發(fā)的時(shí)候,感覺并沒有受cpu核心數(shù)的限制,想啟動(dòng)線程就啟動(dòng)線程,哪怕是在單核CPU上,為什么?這是因?yàn)椴僮飨到y(tǒng)提供了一種CPU時(shí)間片輪轉(zhuǎn)機(jī)制。
時(shí)間片輪轉(zhuǎn)調(diào)度是一種最古老、最簡單、最公平且使用最廣的算法,又稱RR調(diào)度。每個(gè)進(jìn)程被分配一個(gè)時(shí)間段,稱作它的時(shí)間片,即該進(jìn)程允許運(yùn)行的時(shí)間。
百度百科對(duì)CPU時(shí)間片輪轉(zhuǎn)機(jī)制原理解釋如下:
如果在時(shí)間片結(jié)束時(shí)進(jìn)程還在運(yùn)行,則CPU將被剝奪并分配給另一個(gè)進(jìn)程。如果進(jìn)程在時(shí)間片結(jié)束前阻塞或結(jié)來,則CPU當(dāng)即進(jìn)行切換。調(diào)度程序所要做的就是維護(hù)一張就緒進(jìn)程列表,當(dāng)進(jìn)程用完它的時(shí)間片后,它被移到隊(duì)列的末尾。
時(shí)間片輪轉(zhuǎn)調(diào)度中唯一有趣的一點(diǎn)是時(shí)間片的長度。從一個(gè)進(jìn)程切換到另一個(gè)進(jìn)程是需要定時(shí)間的,包括保存和裝入寄存器值及內(nèi)存映像,更新各種表格和隊(duì)列等。假如進(jìn)程切( processwitch),有時(shí)稱為上下文切換( context switch),需要5ms,再假設(shè)時(shí)間片設(shè)為20ms,則在做完20ms有用的工作之后,CPU將花費(fèi)5ms來進(jìn)行進(jìn)程切換。CPU時(shí)間的20%被浪費(fèi)在了管理開銷上了。
為了提高CPU效率,我們可以將時(shí)間片設(shè)為5000ms。這時(shí)浪費(fèi)的時(shí)間只有0.1%。但考慮到在一個(gè)分時(shí)系統(tǒng)中,如果有10個(gè)交互用戶幾乎同時(shí)按下回車鍵,將發(fā)生什么情況?假設(shè)所有其他進(jìn)程都用足它們的時(shí)間片的話,最后一個(gè)不幸的進(jìn)程不得不等待5s才獲得運(yùn)行機(jī)會(huì)。多數(shù)用戶無法忍受一條簡短命令要5才能做出響應(yīng),同樣的問題在一臺(tái)支持多道程序的個(gè)人計(jì)算機(jī)上也會(huì)發(fā)
結(jié)論可以歸結(jié)如下:時(shí)間片設(shè)得太短會(huì)導(dǎo)致過多的進(jìn)程切換,降低了CPU效率:而設(shè)得太長又可能引起對(duì)短的交互請(qǐng)求的響應(yīng)變差。將時(shí)間片設(shè)為100ms通常是一個(gè)比較合理的折衷
在CPU死機(jī)的情況下,其實(shí)大家不難發(fā)現(xiàn)當(dāng)運(yùn)行一個(gè)程序的時(shí)候把CPU給弄到了100%再不重啟電腦的情況下,其實(shí)我們還是有機(jī)會(huì)把它KⅢ掉的,我想也正是因?yàn)檫@種機(jī)制的緣故。

3、什么是進(jìn)程和線程

進(jìn)程是程序運(yùn)行資源分配的最小單位
進(jìn)程是操作系統(tǒng)進(jìn)行資源分配的最小單位,其中資源包括:CPU、內(nèi)存空間、磁盤10等,同一進(jìn)程中的多條線程共享該進(jìn)程中的全部系統(tǒng)資源,而進(jìn)程和進(jìn)程之間是相互獨(dú)立的。進(jìn)程是具有一定獨(dú)立功能的程序關(guān)于某個(gè)數(shù)據(jù)集合上的一次運(yùn)行活動(dòng),進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位。
進(jìn)程是程序在計(jì)算機(jī)上的一次執(zhí)行活動(dòng)。當(dāng)你運(yùn)行一個(gè)程序,你就啟動(dòng)了一個(gè)進(jìn)程。顯然,程序是死的、靜態(tài)的,進(jìn)程是活的、動(dòng)態(tài)的。進(jìn)程可以分為系統(tǒng)進(jìn)程和用戶進(jìn)程。凡是用于完成操作系統(tǒng)的各種功能的進(jìn)程就是系統(tǒng)進(jìn)程,它們就是處于運(yùn)行狀態(tài)下的操作系統(tǒng)本身,用戶進(jìn)程就是所有由你啟動(dòng)的進(jìn)程。
線程是CPU調(diào)度的最小單位,必須依賴于進(jìn)程而存在
線程是進(jìn)程的一個(gè)實(shí)體,是CPU調(diào)度和分派的基本單位,它是比進(jìn)程更小的、能獨(dú)立運(yùn)行的基本單位。線程自己基本上不擁有系統(tǒng)資源,只擁有一點(diǎn)在運(yùn)行中必不可少的資源(如程序計(jì)數(shù)器,一組寄存器和棧),但是它可與同屬一個(gè)進(jìn)程的其他的線程共享進(jìn)程所擁有的全部資源。
線程無處不在
任何一個(gè)程序都必須要?jiǎng)?chuàng)建線程,特別是Java不管任何程序都必須啟動(dòng)一個(gè)main函數(shù)的主線程; Java Web開發(fā)里面的定時(shí)任務(wù)、定時(shí)器、JSP和 Servlet、異步消息處理機(jī)制,遠(yuǎn)程訪問接口RM等,任何一個(gè)監(jiān)聽事件, onclick的觸發(fā)事件等都離不開線程和并發(fā)的知識(shí)。

4、并行與并發(fā)

我們舉個(gè)例子,如果有條高速公路A上面并排有8條車道,那么最大的并行車輛就是8輛此條高速公路A同時(shí)并排行走的車輛小于等于8輛的時(shí)候,車輛就可以并行運(yùn)行。CPU也是這個(gè)原理,一個(gè)CPU相當(dāng)于一個(gè)高速公路A,核心數(shù)或者線程數(shù)就相當(dāng)于并排可以通行的車道;而多個(gè)CPU就相當(dāng)于并排有多條高速公路,而每個(gè)高速公路并排有多個(gè)車道。
當(dāng)談?wù)摬l(fā)的時(shí)候一定要加個(gè)單位時(shí)間,也就是說單位時(shí)間內(nèi)并發(fā)量是多少?離開了單位時(shí)間其實(shí)是沒有意義的。
俗話說,一心不能二用,這對(duì)計(jì)算機(jī)也一樣,原則上一個(gè)CPU只能分配給一個(gè)進(jìn)程,以便運(yùn)行這個(gè)進(jìn)程。我們通常使用的計(jì)算機(jī)中只有一個(gè)CPU,也就是說只有一顆心,要讓它一心多用同時(shí)運(yùn)行多個(gè)進(jìn)程,就必須使用并發(fā)技術(shù)。實(shí)現(xiàn)并發(fā)技術(shù)相當(dāng)復(fù)雜,最容易理解的是“時(shí)間片輪轉(zhuǎn)進(jìn)程調(diào)度算法”。
綜合來說:
并發(fā):指應(yīng)用能夠交替執(zhí)行不同的任務(wù),比如單CPU核心下執(zhí)行多線程并非是同時(shí)執(zhí)行多個(gè)任務(wù),如果你開兩個(gè)線程執(zhí)行,就是在你幾乎不可能察覺到的速度不斷去切換這兩個(gè)任務(wù),已達(dá)到"同時(shí)執(zhí)行效果",其實(shí)并不是的,只是計(jì)算機(jī)的速度太快,我們無法察覺到而已.
并行:指應(yīng)用能夠同時(shí)執(zhí)行不同的任務(wù),例:吃飯的時(shí)候可以邊吃飯邊打電話,這兩件事情可以同時(shí)執(zhí)行
兩者區(qū)別:一個(gè)是交替執(zhí)行,一個(gè)是同時(shí)執(zhí)行。

5、為何我們需要多線程編程

由于多核多線程的CPU的誕生,多線程、高并發(fā)的編程越來越受重視和關(guān)注。多線程可以給程序帶來如下好處。
(1)充分利用CPU的資源
從上面的CPU的介紹,可以看的出來,現(xiàn)在市面上沒有CPU的內(nèi)核不使用多線程并發(fā)機(jī)制的,特別是服務(wù)器還不止一個(gè)CPU,如果還是使用單線程的技術(shù)做思路,明顯就out了。因?yàn)槌绦虻幕菊{(diào)度單元是線程,并且一個(gè)線程也只能在一個(gè)CPU的一個(gè)核的一個(gè)線程跑,如果你是個(gè)i3的CPU的話,最差也是雙核心4線程的運(yùn)算能力:如果是一個(gè)線程的程序的話,那是要浪費(fèi)3/4的CPU性能:如果設(shè)計(jì)一個(gè)多線程的程序的話,那它就可以同時(shí)在多個(gè)CPU的多個(gè)核的多個(gè)線程上跑,可以充分地利用CPU,減少CPU的空閑時(shí)間,發(fā)揮它的運(yùn)算能力,提高并發(fā)量。
就像我們平時(shí)坐地鐵一樣,很多人坐長線地鐵的時(shí)候都在認(rèn)真看書,而不是為了坐地鐵而坐地鐵,到家了再去看書,這樣你的時(shí)間就相當(dāng)于有了兩倍。這就是為什么有些人時(shí)間很充裕,而有些人老是說沒時(shí)間的一個(gè)原因,工作也是這樣,有的時(shí)候可以并發(fā)地去做幾件事情,充分利用我們的時(shí)間,CPU也是一樣,也要充分利用。
(2)加快響應(yīng)用戶的時(shí)間
比如我們經(jīng)常用的迅雷下載,都喜歡多開幾個(gè)線程去下載,誰都不愿意用一個(gè)線程去下載,為什么呢?答案很簡單,就是多個(gè)線程下載快啊。
我們?cè)谧龀绦蜷_發(fā)的時(shí)候更應(yīng)該如此,特別是我們做互聯(lián)網(wǎng)項(xiàng)目,網(wǎng)頁的響應(yīng)時(shí)間若提升1s,如果流量大的話,就能增加不少轉(zhuǎn)換量。做過高性能web前端調(diào)優(yōu)的都知道,要將靜態(tài)資源地址用兩三個(gè)子域名去加載,為什么?因?yàn)槊慷嘁粋€(gè)子域名,瀏覽器在加載你的頁面的時(shí)候就會(huì)多開幾個(gè)線程去加載你的頁面資源,提升網(wǎng)站的響應(yīng)速度。多線程,高并發(fā)真的是無處不在。
(3)可以使你的代碼模塊化,異步化,簡單化
例如我們?cè)谧?Android程序開發(fā)的時(shí)候,主線程的UI展示部分是一塊主代碼程序部分,但是UI上的按鈕用相應(yīng)事件的處理程序就可以做個(gè)單獨(dú)的模塊程序拿出來。這樣既增加了異步的操,又使程序模塊化,清晰化和簡單化。
時(shí)下最流行的異步程序處理機(jī)制,正是多線程、并發(fā)程序最好的應(yīng)用例子。
多線程應(yīng)用開發(fā)的好處還有很多,大家在日后的代碼編寫過程中可以慢慢體會(huì)它的魅力。

6、多線程程序需要注意事項(xiàng)

線程之間的安全性
從前面的章節(jié)中我們都知道,在同一個(gè)進(jìn)程里面的多線程是資源共享的,也就是都可以訪問同一個(gè)內(nèi)存地址當(dāng)中的一個(gè)變量。例如:若每個(gè)線程中對(duì)全局變量、靜態(tài)變量只有讀操作,而無寫操作,一般來說,這個(gè)全局變量是線程安全的:若有多個(gè)線程同時(shí)執(zhí)行寫操作,一般都需要考慮線程同步,否則就可能影響線程安全。
線程之間的死循環(huán)過程
為了解決線程之間的安全性引入了Java的鎖機(jī)制,而一不小心就會(huì)產(chǎn)生Java線程死鎖的多線程問題,因?yàn)椴煌木€程都在等待那些根本不可能被釋放的鎖,從而導(dǎo)致所有的工作都無法完成。假設(shè)有兩個(gè)線程,分別代表兩個(gè)饑餓的人,他們必須共享刀叉并輪流吃飯。他們都需要獲得兩個(gè)鎖:共享刀和共享叉的鎖。
假如線程A獲得了刀,而線程B獲得了叉。線程A就會(huì)進(jìn)入阻塞狀態(tài)來等待獲得叉,而線程B則阻塞來等待線程A所擁有的刀。這只是人為設(shè)計(jì)的例子,但盡管在運(yùn)行時(shí)很難探測到,這類情況卻時(shí)常發(fā)生
線程太多了會(huì)將服務(wù)器資源耗盡形成死機(jī)當(dāng)機(jī)
線程數(shù)太多有可能造成系統(tǒng)創(chuàng)建大量線程而導(dǎo)致消耗完系統(tǒng)內(nèi)存以及CPU的“過渡切換”,造成系統(tǒng)的死機(jī),那么我們?cè)撊绾谓鉀Q這類問題呢?
某些系統(tǒng)資源是有限的,如文件描述符。多線程程序可能耗盡資源,因?yàn)槊總€(gè)線程都可能希望有一個(gè)這樣的資源。如果線程數(shù)相當(dāng)大,或者某個(gè)資源的侯選線程數(shù)遠(yuǎn)遠(yuǎn)超過了可用的資源數(shù)則最好使用資源池。一個(gè)最好的示例是數(shù)據(jù)庫連接池。只要線程需要使用一個(gè)數(shù)據(jù)庫連接,它就從池中取出一個(gè),使用以后再將它返回池中。資源池也稱為資源庫。這里先有一個(gè)概念,后面會(huì)
多線程應(yīng)用開發(fā)的注意事項(xiàng)很多,希望大家在日后的工作中可以慢慢體會(huì)它的危險(xiǎn)所在。

7、線程啟動(dòng)方式

1、通過Thread方式啟動(dòng)

public class UseThread extends Thread{
        @Override
        public void run() {
            super.run();
            //do my work
            System.out.println("I am extends Thread");
        }
    }

UseThread useThread = new UseThread();
useThread.start();
運(yùn)行結(jié)果:I am extends Thread

2、通過Runnable方式啟動(dòng)

public class UseRun implements Runnable{

        @Override
        public void run() {
            System.out.println("I am implements Runnable");
        }
    }

NewThread.UseRun useRun = new NewThread.UseRun();
new Thread(useRun).start();
運(yùn)行結(jié)果:I am implements Runnable

3、通過Callable方式啟動(dòng)

/*實(shí)現(xiàn)Callable接口,允許有返回值*/
public class UseCall implements Callable<String>{
        @Override
        public String call() throws Exception {
            System.out.println("I am implements Callable");
            //TODO 模擬在做耗時(shí)任務(wù)
            Thread.sleep(4000);
            return "CallResult";
        }
    }

FutureTask<String> futureTask = new FutureTask<>(useCall);
new Thread(futureTask).start();
System.out.println(futureTask.get());
運(yùn)行結(jié)果:I am implements Callable
運(yùn)行結(jié)果:CallResult(4S后才輸出)

Callable、Future和FutureTask 備注:通過FutureTask可以獲取線程結(jié)束后的結(jié)果。
V get() :獲取異步執(zhí)行的結(jié)果,如果沒有結(jié)果可用,此方法會(huì)阻塞直到異步計(jì)算完成。
V get(Long timeout , TimeUnit unit) :獲取異步執(zhí)行結(jié)果,如果沒有結(jié)果可用,此方法會(huì)阻塞,但是會(huì)有時(shí)間限制,如果阻塞時(shí)間超過設(shè)定的timeout時(shí)間,該方法將拋出異常。
boolean isDone() :如果任務(wù)執(zhí)行結(jié)束,無論是正常結(jié)束或是中途取消還是發(fā)生異常,都返回true。
boolean isCanceller() :如果任務(wù)完成前被取消,則返回true。
boolean cancel(boolean mayInterruptRunning) :如果任務(wù)還沒開始,執(zhí)行cancel(...)方法將返回false;如果任務(wù)已經(jīng)啟動(dòng),執(zhí)行cancel(true)方法將以中斷執(zhí)行此任務(wù)線程的方式來試圖停止任務(wù),如果停止成功,返回true;當(dāng)任務(wù)已經(jīng)啟動(dòng),執(zhí)行cancel(false)方法將不會(huì)對(duì)正在執(zhí)行的任務(wù)線程產(chǎn)生影響(讓線程正常執(zhí)行到完成),此時(shí)返回false;當(dāng)任務(wù)已經(jīng)完成,執(zhí)行cancel(...)方法將返回false。

Callable、Future和FutureTask:https://blog.csdn.net/pange1991/article/details/80967701

8、線程安全停止方式

1、useThread.interrupt();
協(xié)作式停止,線程不一定停止
2、useThread.isInterrupted();
判斷線程是否中止
3、Thread.interrupted();
也是判斷線程是否中止,備注:每一次調(diào)用Thread.interrupted()之后,會(huì)把中斷標(biāo)志置為false。

9、run()和start()區(qū)別

Thread thread = new Thread();
thread.run();//就在當(dāng)前線程運(yùn)行,并沒有單獨(dú)開啟線程
thread.start();//單獨(dú)開啟線程

9、join():把指定線程插入到當(dāng)前線程,用于自線程順序執(zhí)行
10、yield():把當(dāng)前線程的CPU執(zhí)行權(quán)讓出來,但是并不一定讓給其他線程,有可能系統(tǒng)調(diào)度吧CPU使用權(quán)又交給當(dāng)前線程。

1、開啟線程的數(shù)量是否對(duì)程序有影響?
答:雖然引入時(shí)間片輪轉(zhuǎn)機(jī)制,可以支持高并發(fā)線程開發(fā)。但是要考慮到:每個(gè)線程都是在??臻g分配內(nèi)存(1M),如果線程數(shù)量太多了也是很有影響的的,時(shí)間片輪轉(zhuǎn)機(jī)制在進(jìn)行線程的啟動(dòng)、停止(上下文切換)是很耗費(fèi)時(shí)間的,如果線程的數(shù)量太多,大大降低了CPU執(zhí)行效率的。
2、在run()方法中如果存在Thread.sleep()使用;在線程執(zhí)行過程中,調(diào)用wai t()、interrupt()方法時(shí)會(huì)拋出InterruptedException異常,并不會(huì)停止線程的運(yùn)行,而是繼續(xù)運(yùn)行,如果想讓線程停止,則需要在捕獲到異常之后,再次調(diào)用interrupt();
3、在Java層面能否強(qiáng)制中斷線程?
stop()-已經(jīng)過時(shí),強(qiáng)制中斷會(huì)導(dǎo)致線程相關(guān)的資源沒有釋放
interrupt()-不一定能終止線程

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容