線程基礎、線程之間的共享和協作

1、線程基礎、線程之間的共享和協作

基礎概念

什麼是進程和線程

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

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

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

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

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

線程無處不在

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

CPU核心數和線程數的關係

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

多線程: Simultaneous Multithreading.簡稱SMT.讓同一個處理器上的多個線程同步執行並共享處理器的執行資源。

核心數、線程數:目前主流CPU都是多核的。增加核心數目就是爲了增加線程數,因爲操作系統是通過線程來執行任務的,一般情況下它們是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Ⅲ掉的,我想也正是因爲這種機制的緣故。

澄清並行和併發

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

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

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

綜合來說:

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

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

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

https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1554817822231&di=006a1f4aadd6b6290cdf004dfe86df63&imgtype=0&src=http%3A%2F%2Fs8.51cto.com%2Fwyfs01%2FM00%2F0F%2FED%2FwKioOVHU01Xg-WRYAAB1FK7yRTw313.jpg-wh_651x-s_2888030331.jpg

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

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

(1)充分利用CPU的資源

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

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

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

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

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

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

例如我們實現電商系統,下訂單和給用戶發送短信、郵件就可以進行拆分,將給用戶發送短信、郵件這兩個步驟獨立爲單獨的模塊,並交給其他線程去執行。這樣既增加了異步的操作,提升了系統性能,又使程序模塊化,清晰化和簡單化。

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

多線程程序需要注意事項

(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.start

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

Thread和Runnable的區別

Thread纔是Java裏對線程的唯一抽象,Runnable只是對任務(業務邏輯)的抽象。Thread可以接受任意一個Runnable的實例並執行。

中止

線程自然終止

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

stop

暫停、恢復和停止操作對應在線程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( )的線程不一定就會持有鎖,我們完全可以在釋放鎖後再調用yield方法。

所有執行yield()的線程有可能在進入到就緒狀態後會被操作系統再次選中馬上又被執行。

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

join方法

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

線程的優先級

在Java線程中,通過一個整型成員變量priority來控制優先級,優先級的範圍從1~10,在線程構建的時候可以通過setPriority(int)方法來修改優先級,默認優先級是5,優先級高的線程分配時間片的數量要多於優先級低的線程。

設置線程優先級時,針對頻繁阻塞(休眠或者I/O操作)的線程需要設置較高優先級,而偏重計算(需要較多CPU時間或者偏運算)的線程則設置較低的優先級,確保處理器不會被獨佔。在不同的JVM以及操作系統上,線程規劃會存在差異,有些操作系統甚至會忽略對線程優先級的設定。

守護線程

Daemon(守護)線程是一種支持型線程,因爲它主要被用作程序中後臺調度以及支持性工作。這意味着,當一個Java虛擬機中不存在Daemon線程的時候,Java虛擬機將會退出。可以通過調用Thread.setDaemon(true)將線程設置爲Daemon線程。我們一般用不上,比如垃圾回收線程就是Daemon線程。

Daemon線程被用作完成支持性工作,但是在Java虛擬機退出時Daemon線程中的finally塊並不一定會執行。在構建Daemon線程時,不能依靠finally塊中的內容來確保執行關閉或清理資源的邏輯。

線程間的共享和協作

線程間的共享

synchronized內置鎖

線程開始運行,擁有自己的棧空間,就如同一個腳本一樣,按照既定的代碼一步一步地執行,直到終止。但是,每個運行中的線程,如果僅僅是孤立地運行,那麼沒有一點兒價值,或者說價值很少,如果多個線程能夠相互配合完成工作,包括數據之間的共享,協同處理事情。這將會帶來巨大的價值。

Java支持多個線程同時訪問一個對象或者對象的成員變量,關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻,只能有一個線程處於方法或者同步塊中,它保證了線程對變量訪問的可見性和排他性,又稱爲內置鎖機制。

對象鎖和類鎖:

對象鎖是用於對象實例方法,或者一個對象實例上的,類鎖是用於類的靜態方法或者一個類的class對象上的。我們知道,類的對象實例可以有很多個,但是每個類只有一個class對象,所以不同對象實例的對象鎖是互不干擾的,但是每個類只有一個類鎖。

但是有一點必須注意的是,其實類鎖只是一個概念上的東西,並不是真實存在的,類鎖其實鎖的是每個類的對應的class對象。類鎖和對象鎖之間也是互不干擾的。

 

發佈了22 篇原創文章 · 獲贊 5 · 訪問量 2866
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章