java線程雜談

基礎概念

CPU核心數和線程數的關係

多核心:也指單芯片多處理器( Chip Multiprocessors,簡稱CMP),CMP是由美國斯坦福大學提出的,其思想是將大規模並行處理器中的SMP(對稱多處理器)集成到同一芯片內,各個處理器並行執行不同的進程。這種依靠多個CPU同時並行地運行程序是實現超高速計算的一個重要方向,稱爲並行處理

多線程: Simultaneous Multithreading.簡稱SMT.SMT可通過複製處理器上的結構狀態,讓同一個處理器上的多個線程同步執行並共享處理器的執行資源可最大限度地實現寬發射、亂序的超標量處理,提高處理器運算部件的利用率,緩和由於數據相關或 Cache未命中帶來的訪問內存延時。

核心數、線程數:目前主流CPU有雙核、三核和四核,六核也在2010年發佈。增加核心數目就是爲了增加線程數,因爲操作系統是通過線程來執行任務的,一般情況下它們是1:1對應關係,也就是說四核CPU一般擁有四個線程。但 Intel引入超線程技術後,使核心數與線程數形成1:2的關係

CPU時間片輪轉機制

我們平時在開發的時候,感覺並沒有受cpu核心數的限制,想啓動線程就啓動線程,哪怕是在單核CPU上,爲什麼?這是因爲操作系統提供了一種CPU時間片輪轉機制。

時間片輪轉調度是一種最古老、最簡單、最公平且使用最廣的算法,又稱RR調度。每個進程被分配一個時間段,稱作它的時間片,即該進程允許運行的時間。

百度百科對CPU時間片輪轉機制原理解釋如下:

如果在時間片結束時進程還在運行,則CPU將被剝奪並分配給另一個進程。如果進程在時間片結束前阻塞或結來,則CPU當即進行切換。調度程序所要做的就是維護一張就緒進程列表,當進程用完它的時間片後,它被移到隊列的末尾

時間片輪轉調度中唯一有趣的一點是時間片的長度。從一個進程切換到另一個進程是需要定時間的,包括保存和裝入寄存器值及內存映像,更新各種表格和隊列等。假如進程切( processwitch),有時稱爲上下文切換( context switch),需要5ms,再假設時間片設爲20ms,則在做完20ms有用的工作之後,CPU將花費5ms來進行進程切換。CPU時間的20%被浪費在了管理開銷上了。

爲了提高CPU效率,我們可以將時間片設爲5000ms。這時浪費的時間只有0.1%。但考慮到在一個分時系統中,如果有10個交互用戶幾乎同時按下回車鍵,將發生什麼情況?假設所有其他進程都用足它們的時間片的話,最後一個不幸的進程不得不等待5s才獲得運行機會。多數用戶無法忍受一條簡短命令要5才能做出響應,同樣的問題在一臺支持多道程序的個人計算機上也會發

結論可以歸結如下:時間片設得太短會導致過多的進程切換,降低了CPU效率:而設得太長又可能引起對短的交互請求的響應變差。將時間片設爲100ms通常是一個比較合理的折衷。

在CPU死機的情況下,其實大家不難發現當運行一個程序的時候把CPU給弄到了100%再不重啓電腦的情況下,其實我們還是有機會把它KⅢ掉的,我想也正是因爲這種機制的緣故。

什麼是進程和線程

進程是程序運行資源分配的最小單位

進程是操作系統進行資源分配的最小單位,其中資源包括:CPU、內存空間、磁盤10等,同一進程中的多條線程共享該進程中的全部系統資源,而進程和進程之間是相互獨立的。進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位。

進程是程序在計算機上的一次執行活動。當你運行一個程序,你就啓動了一個進程。顯然,程序是死的、靜態的,進程是活的、動態的。進程可以分爲系統進程和用戶進程。凡是用於完成操作系統的各種功能的進程就是系統進程,它們就是處於運行狀態下的操作系統本身,用戶進程就是所有由你啓動的進程。

線程是cPU調度的最小單位,必須依賴於進程而存在

線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的、能獨立運行的基本單位。線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。

線程無處不在

任何一個程序都必須要創建線程,特別是Java不管任何程序都必須啓動一個main函數的主線程; Java Web開發裏面的定時任務、定時器、JSP和 Servlet、異步消息處理機制,遠程訪問接口RM等,任何一個監聽事件, onclick的觸發事件等都離不開線程和併發的知識。

澄清並行和併發

我們舉個例子,如果有條高速公路A上面並排有8條車道,那麼最大的並行車輛就是8輛此條高速公路A同時並排行走的車輛小於等於8輛的時候,車輛就可以並行運行。CPU也是這個原理,一個CPU相當於一個高速公路A,核心數或者線程數就相當於並排可以通行的車道;而多個CPU就相當於並排有多條高速公路,而每個高速公路並排有多個車道。

當談論併發的時候一定要加個單位時間,也就是說單位時間內併發量是多少?離開了單位時間其實是沒有意義的。

俗話說,一心不能二用,這對計算機也一樣,原則上一個CPU只能分配給一個進程,以便運行這個進程。我們通常使用的計算機中只有一個CPU,也就是說只有一顆心,要讓它一心多用同時運行多個進程,就必須使用併發技術。實現併發技術相當複雜,最容易理解的是“時間片輪轉進程調度算法”。

綜合來說:

併發:指應用能夠交替執行不同的任務,比如單CPU核心下執行多線程並非是同時執行多個任務,如果你開兩個線程執行,就是在你幾乎不可能察覺到的速度不斷去切換這兩個任務,已達到"同時執行效果",其實並不是的,只是計算機的速度太快,我們無法察覺到而已.

並行:指應用能夠同時執行不同的任務,例:吃飯的時候可以邊吃飯邊打電話,這兩件事情可以同時執行

兩者區別:一個是交替執行,一個是同時執行.

高併發編程的意義、好處和注意事項

由於多核多線程的CPU的誕生,多線程、高併發的編程越來越受重視和關注。多線程可以給程序帶來如下好處。

(1)充分利用CPU的資源

從上面的CPU的介紹,可以看的出來,現在市面上沒有CPU的內核不使用多線程併發機制的,特別是服務器還不止一個CPU,如果還是使用單線程的技術做思路,明顯就out了。因爲程序的基本調度單元是線程,並且一個線程也只能在一個CPU的一個核的一個線程跑,如果你是個i3的CPU的話,最差也是雙核心4線程的運算能力:如果是一個線程的程序的話,那是要浪費3/4的CPU性能:如果設計一個多線程的程序的話,那它就可以同時在多個CPU的多個核的多個線程上跑,可以充分地利用CPU,減少CPU的空閒時間,發揮它的運算能力,提高併發量。

就像我們平時坐地鐵一樣,很多人坐長線地鐵的時候都在認真看書,而不是爲了坐地鐵而坐地鐵,到家了再去看書,這樣你的時間就相當於有了兩倍。這就是爲什麼有些人時間很充裕,而有些人老是說沒時間的一個原因,工作也是這樣,有的時候可以併發地去做幾件事情,充分利用我們的時間,CPU也是一樣,也要充分利用。

(2)加快響應用戶的時間

比如我們經常用的迅雷下載,都喜歡多開幾個線程去下載,誰都不願意用一個線程去下載,爲什麼呢?答案很簡單,就是多個線程下載快啊。

我們在做程序開發的時候更應該如此,特別是我們做互聯網項目,網頁的響應時間若提升1s,如果流量大的話,就能增加不少轉換量。做過高性能web前端調優的都知道,要將靜態資源地址用兩三個子域名去加載,爲什麼?因爲每多一個子域名,瀏覽器在加載你的頁面的時候就會多開幾個線程去加載你的頁面資源,提升網站的響應速度。多線程,高併發真的是無處不在。

(3)可以使你的代碼模塊化,異步化,簡單化

例如我們在做 Android程序開發的時候,主線程的UI展示部分是一塊主代碼程序部分,但是UI上的按鈕用相應事件的處理程序就可以做個單獨的模塊程序拿出來。這樣既增加了異步的操,又使程序模塊化,清晰化和簡單化。

時下最流行的異步程序處理機制,正是多線程、併發程序最好的應用例子。

多線程應用開發的好處還有很多,大家在日後的代碼編寫過程中可以慢慢體會它的魅力。

 

多線程程序需要注意事項

(1)線程之間的安全性

從前面的章節中我們都知道,在同一個進程裏面的多線程是資源共享的,也就是都可以訪問同一個內存地址當中的一個變量。例如:若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的:若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則就可能影響線程安全。

 (2)線程之間的死循環過程

爲了解決線程之間的安全性引入了Java的鎖機制,而一不小心就會產生Java線程死鎖的多線程問題,因爲不同的線程都在等待那些根本不可能被釋放的鎖,從而導致所有的工作都無法完成。假設有兩個線程,分別代表兩個飢餓的人,他們必須共享刀叉並輪流吃飯。他們都需要獲得兩個鎖:共享刀和共享叉的鎖。

假如線程A獲得了刀,而線程B獲得了叉。線程A就會進入阻塞狀態來等待獲得叉,而線程B則阻塞來等待線程A所擁有的刀。這只是人爲設計的例子,但儘管在運行時很難探測到,這類情況卻時常發生

(3)線程太多了會將服務器資源耗盡形成死機當機

線程數太多有可能造成系統創建大量線程而導致消耗完系統內存以及CPU的“過渡切換”,造成系統的死機,那麼我們該如何解決這類問題呢?

某些系統資源是有限的,如文件描述符。多線程程序可能耗盡資源,因爲每個線程都可能希望有一個這樣的資源。如果線程數相當大,或者某個資源的侯選線程數遠遠超過了可用的資源數則最好使用資源池。一個最好的示例是數據庫連接池。只要線程需要使用一個數據庫連接,它就從池中取出一個,使用以後再將它返回池中。資源池也稱爲資源庫。這裏先有一個概念,後面會

多線程應用開發的注意事項很多,希望大家在日後的工作中可以慢慢體會它的危險所在。

 

認識Java裏的線程

Java裏的程序天生就是多線程的

一個Java程序從main()方法開始執行,然後按照既定的代碼邏輯執行,看似沒有其他線程參與,但實際上Java程序天生就是多線程程序,因爲執行main()方法的是一個名稱爲main的線程。

[6] Monitor Ctrl-Break //監控Ctrl-Break中斷信號的

[5] Attach Listener //內存dump,線程dump,類信息統計,獲取系統屬性等

[4] Signal Dispatcher  // 分發處理髮送給JVM信號的線程

[3] Finalizer  // 調用對象finalize方法的線程

[2] Reference Handler//清除Reference的線程

[1] main //main線程,用戶程序入口

線程的啓動與中止

啓動

啓動線程的方式有:

1、X extends Thread;,然後X.run

2、X implements  Runnable;然後交給Thread運行

3、X implements  Callable;然後交給Thread運行

第1、2方式都有一個缺陷就是:在執行完任務之後無法獲取執行結果。從Java 1.5開始,就提供了Callable和Future,通過它們可以在任務執行完畢之後得到任務執行結果。

參見代碼:cn.enjoyedu.concurrent.NewThread

Callable、Future和FutureTask

Runnable是一個接口,在它裏面只聲明瞭一個run()方法,由於run()方法返回值爲void類型,所以在執行完任務之後無法返回任何結果。

Callable位於java.util.concurrent包下,它也是一個接口,在它裏面也只聲明瞭一個方法,只不過這個方法叫做call(),這是一個泛型接口,call()函數返回的類型就是傳遞進來的V類型。

Future就是對於具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成、獲取結果。必要時可以通過get方法獲取執行結果,該方法會阻塞直到任務返回結果。

 

因爲Future只是一個接口,所以是無法直接用來創建對象使用的,因此就有了下面的FutureTask。

 

 

FutureTask類實現了RunnableFuture接口,RunnableFuture繼承了Runnable接口和Future接口,而FutureTask實現了RunnableFuture接口。所以它既可以作爲Runnable被線程執行,又可以作爲Future得到Callable的返回值。

事實上,FutureTask是Future接口的一個唯一實現類。

要new一個FutureTask的實例,有兩種方法

 

中止

線程自然終止:要麼是run執行完成了,要麼是拋出了一個未處理的異常導致線程提前結束。

手動中止

暫停、恢復和停止操作對應在線程Thread的API就是suspend()resume()stop()。但是這些API是過期的,也就是不建議使用的。不建議使用的原因主要有:以suspend()方法爲例,在調用後,線程不會釋放已經佔有的資源(比如鎖),而是佔有着資源進入睡眠狀態,這樣容易引發死鎖問題。同樣,stop()方法在終結一個線程時不會保證線程的資源正常釋放,通常是沒有給予線程完成資源釋放工作的機會,因此會導致程序可能工作在不確定狀態下。正因爲suspend()、resume()和stop()方法帶來的副作用,這些方法才被標註爲不建議使用的過期方法。

安全的中止則是其他線程通過調用某個線程A的interrupt()方法對其進行中斷操作, 中斷好比其他線程對該線程打了個招呼,“A,你要中斷了”,不代表線程A會立即停止自己的工作,同樣的A線程完全可以不理會這種中斷請求。因爲java裏的線程是協作式的,不是搶佔式的。線程通過檢查自身的中斷標誌位是否被置爲true來進行響應,線程通過方法isInterrupted()來進行判斷是否被中斷,也可以調用靜態方法Thread.interrupted()來進行判斷當前線程是否被中斷,不過Thread.interrupted()會同時將中斷標識位改寫爲false。

如果一個線程處於了阻塞狀態(如線程調用了thread.sleep、thread.join、thread.wait、),則在線程在檢查中斷標示時如果發現中斷標示爲true,則會在這些阻塞方法調用處拋出InterruptedException異常,並且在拋出異常後會立即將線程的中斷標示位清除,即重新設置爲false。

不建議自定義一個取消標誌位來中止線程的運行。因爲run方法裏有阻塞調用時會無法很快檢測到取消標誌,線程必須從阻塞調用返回後,纔會檢查這個取消標誌。這種情況下,使用中斷會更好,因爲,一、一般的阻塞方法,如sleep等本身就支持中斷的檢查,二、檢查中斷位的狀態和檢查取消標誌位沒什麼區別,用中斷位的狀態還可以避免聲明取消標誌位,減少資源的消耗。

注意:處於死鎖狀態的線程無法被中斷

對Java裏的線程再多一點點認識

深入理解run()和start()

Thread類是Java裏對線程概念的抽象,可以這樣理解:我們通過new Thread()其實只是new出一個Thread的實例,還沒有操作系統中真正的線程掛起鉤來。只有執行了start()方法後,才實現了真正意義上的啓動線程。

start()方法讓一個線程進入就緒隊列等待分配cpu,分到cpu後才調用實現的run()方法,start()方法不能重複調用。

而run方法是業務邏輯實現的地方,本質上和任意一個類的任意一個成員方法並沒有任何區別,可以重複執行,可以被單獨調用。

其他的線程方法

yield()方法:使當前線程讓出CPU佔有權,但讓出的時間是不可設定的。也不會釋放鎖資源,所有執行yield()的線程有可能在進入到可執行狀態後馬上又被執行。

join方法:把指定的線程加入到當前線程,可以將兩個交替執行的線程合併爲順序執行的線程。比如在線程B中調用了線程A的Join()方法,直到線程A執行完畢後,纔會繼續執行線程B。

wait()/notify()/notifyAll():後面會單獨講述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章