Java線程之間的共享與協作詳解

前言

在系列文章開始之前,我們首先瞭解一下線程的重要性: ​​線程​​(Thread)是“進程”中某個單一順序的控制流。也被稱為輕量進程(lightweight

processes)。計算機科學術語,指運行中的程序的調度單位。

所有的程序中,都有線程

一、進程和線程

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

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

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

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

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

3、線程無處不在

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

二、CPU 核心數和線程數的關系

1、多核心

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

2、多線程

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

3、核心數、線程數

核心數、線程數:目前主流CPU都是多核的。增加核心數目的就是為瞭增加線程數,因為操作系統是通過線程來執行任務的,一般情況下它們是1:1對應關系,也就是說四核 CPU一般擁有四個線程。但Intel引入超線程技術後,使核心數與線程數形成瞭1:2的關系。

三、CPU 時間片輪轉機制

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

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

四、並行和並發

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

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

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

1、並發

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

2、並行

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

**並發和並行兩者的區別就是:一個是交替執行,一個是同時執行**

五、高並發編程

由於多核CPU的誕生,多線程、高並發的編程越來越受重視和關註。

1、CPU 資源利用的充分

從上面CPU的介紹,可以看出現在市面上沒有CPU的內核不使用多線程並發機制的,特別是服務器還不止一個CPU。程序的基本調度單元是線程,一個線程也隻能在一個一個CPU 的一個核的一個線程跑,如果你是個i3的CPU的話,最差也是雙核心4線程的運算能力:如果是一個線程的話,那就會浪費釣3/4的CPU性能:如果設計一個多線程的話,那它就可以同時在多個CPU 的多個核的多個線程上跑,可以充分的利用CPU,減少CPU的空閑時間,發揮它的運算能力,提高並發量。

2、加快用戶響應時間

比如我們經常使用的下載功能,很多朋友都會開通某一個會員,因為會員版本啟用瞭多個線程去下載,誰都無法忍受一個線程去下,為什麼呢?因為多線程下載快啊。

我們做程序開發的時候,網頁速度提升1s,如果用戶量大的話,就能增加不少轉換量。我們經常瀏覽的網頁中,瀏覽器在加載頁面的時候,都會去多開幾個線程去加載網絡資源,提升網站的相應速度。多線程和高並發,在計算機中,無處不在。

3、使代碼模塊化、異步化、簡單化

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

六、多線程註意事項

1、線程之間的安全性

從前面的章節中我們都知道瞭,在同一個進程裡面的多線程是資源共享的,也就是都可以訪問同一個內存地址當中的一個變量。

例如:若每個線程中對全局變量、靜態變量隻讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則就可能影響線程安全。

2、線程之間的死鎖

為瞭解決線程之間的安全性引入瞭Java 鎖的機制,而一不小心就會產生Java 線程死鎖的多線程問題,因為不同的線程都在等待哪些根本不可能被釋放的鎖,從而導致所有的工作都無法完成。

假設有兩個饑餓的人,他們必須共享刀叉並輪流吃飯,他們都需要獲得兩個鎖,共享刀和共享叉。假如線程A獲得瞭刀,而線程B獲得瞭叉。線程A就會進入阻塞狀態來等待獲得叉,而線程B則主帥來等待線程A所擁有的刀。這隻是人為設計的例子,單盡管在運行時很難探測到,這類情況卻時常發生。

3、線程多瞭會將服務資源耗盡形成死機、當機

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

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

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

七、多線程註意事項

線程之間相互配合,完成某項工作,比如:一個線程修改瞭一個對象的值, 而另一個線程感知到瞭變化,然後進行相應的操作,整個過程開始於一個線程, 而最終執行又是另一個線程。前者是生產者,後者就是消費者,這種模式隔離瞭 “做什麼”(what)和“怎麼做”(How),簡單的辦法是讓消費者線程不斷地 循環檢查變量是否符合預期在 while 循環中設置不滿足的條件,如果條件滿足則 退出 while 循環,從而完成消費者的工作。

卻存在如下問題:

  • 難以確保及時性。
  • 難以降低開銷。如果降低睡眠的時間,比如休眠 1 毫秒,這樣消費者能 更加迅速地發現條件變化,但是卻可能消耗更多的處理器資源,造成瞭無端的浪費。

等待/通知機制:是指一個線程 A 調用瞭對象 O 的 wait()方法進入等待狀態,而另一個線程 B 調用瞭對象 O 的 notify()或者 notifyAll()方法,線程 A 收到通知後從對象 O 的 wait() 方法返回,進而執行後續操作。上述兩個線程通過對象 O 來完成交互,而對象 上的 wait()和 notify/notifyAll()的關系就如同開關信號一樣,用來完成等待方和通 知方之間的交互工作。

notify():通知一個在對象上等待的線程,使其從 wait 方法返回,而返回的前提是該線程獲取到瞭對象的鎖,沒有獲得鎖的線程重新進入 WAITING 狀態。

notifyAll():通知所有等待在該對象上的線程

wait():調用該方法的線程進入 WAITING 狀態,隻有等待另外線程的通知或被中斷 才會返回.需要註意,調用 wait()方法後,會釋放對象的鎖。

wait(long):超時等待一段時間,這裡的參數時間是毫秒,也就是等待長達n 毫秒,如果沒有 通知就超時返回。

wait (long,int):對於超時時間更細粒度的控制,可以達到納秒

等待和通知的標準范式 等待方遵循如下原則。

  • 獲取對象的鎖。
  • 如果條件不滿足,那麼調用對象的 wait()方法,被通知後仍要檢查條件。
  • 條件滿足則執行對應的邏輯。

通知方遵循如下原則:

  • 獲得對象的鎖。
  • 改變條件。
  • 通知所有等待在對象上的線程。

​​在調用 wait()、notify()系列方法之前,線程必須要獲得該對象的對象級別鎖,即隻能在同步方法或同步塊中調用 wait()方法、notify()系列方法,進 入 wait()方法後,當前線程釋放鎖,在從 wait()返回前,線程與其他線程競 爭重新獲得鎖,執行 notify()系列方法的線程退出調用瞭 notifyAll 的 synchronized 代碼塊的時候後,他們就會去競爭。如果其中一個線程獲得瞭該對象鎖,它就會 繼續往下執行,在它退出 synchronized 代碼塊,釋放鎖後,其他的已經被喚醒的 線程將會繼續競爭獲取該鎖,一直進行下去,直到所有被喚醒的線程都執行完畢。

notify 和 notifyAll 應該用誰

盡可能用 notifyAll(),謹慎使用 notify(),因為 notify()隻會喚醒一個線程,我們無法確保被喚醒的這個線程一定就是我們需要喚醒的線程。

到此這篇關於Java線程之間的共享與協作詳解的文章就介紹到這瞭,更多相關Java線程共享與協作內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: