多執行緒-day-08多執行緒和執行緒併發工具總結
目錄
4.5 Callable、Future、FutureTask
正文:
執行緒基礎、執行緒之間的共享和協作
1,CPU核心數和執行緒數之間的關係
我們在選購電腦的時候,CPU是一個需要考慮到核心因素,因為它決定了電腦的效能等級。CPU從早期的單核,發展到現在的雙核,多核。CPU除了核心數之外,還有執行緒數之說,下面就來解釋一下CPU的核心數與執行緒數的關係和區別。
簡單地說,CPU的核心數是指物理上,也就是硬體上存在著幾個核心。比如,雙核就是包括2個相對獨立的CPU核心單元組,四核就包含4個相對獨立的CPU核心單元組,等等,依次類推。
執行緒數是一種邏輯的概念,簡單地說,就是模擬出的CPU核心數。比如,可以通過一個CPU核心數模擬出2執行緒的CPU,也就是說,這個單核心的CPU被模擬成了一個類似雙核心CPU的功能。我們從工作管理員的效能標籤頁中看到的是兩個CPU。
比如Intel 賽揚G460是單核心,雙執行緒的CPU,Intel 酷i3 3220是雙核心 四執行緒,Intel 酷睿i7 4770K是四核心 八執行緒 ,Intel 酷睿i5 457睿0是四核心 四執行緒等等。
對於一個CPU,執行緒數總是大於或等於核心數的。一個核心最少對應一個執行緒,但通過超執行緒技術,一個核心可以對應兩個執行緒,也就是說它可以同時執行兩個執行緒。
CPU的執行緒數概念僅僅只針對Intel的CPU才有用,因為它是通過Intel超執行緒技術來實現的,最早應用在Pentium4上。如果沒有超執行緒技術,一個CPU核心對應一個執行緒。所以,對於AMD的CPU來說,只有核心數的概念,沒有執行緒數的概念。
CPU之所以要增加執行緒數,是源於多工處理的需要。執行緒數越多,越有利於同時執行多個程式,因為執行緒數等同於在某個瞬間CPU能同時並行處理的任務數。
在Windows中,在cmd命令中輸入“wmic”,然後在出現的新視窗中輸入“cpu get *”即可檢視物理CPU數、CPU核心數、執行緒數。其中,
Name:表示物理CPU數
NumberOfCores:表示CPU核心數
NumberOfLogicalProcessors:表示CPU執行緒數
因此簡單總結為:
①、一塊CPU只有一塊處理器
②、Inter提出了多核處理器
③、CPU核心數 和 執行緒數 是 1:1 的關係
④、Inter提出了超執行緒,CPU核心數 和 執行緒數 是 1:2 的關係
⑤、CPU同一時間只能執行16個執行緒
2、CPU時間片輪轉機制
①、RR排程:首先將所有就緒的佇列按FCFS策略排成一個就緒佇列,然後系統設定一定的時間片,每次給隊首作業分配時間片。如果此作業執行結束,即使時間片沒用完,立刻從佇列中去除此作業,並給下一個作業分配新的時間片;如果作業時間片用完沒有執行結束,則將此作業重新加入就緒佇列尾部等待排程。
執行如下圖:
②、CPU時間片輪轉機制可能會導致上下文切換。
CPU上下文切換詳解
上下文切換(有時也稱做程序切換或任務切換)是指CPU從一個程序或執行緒切換到另一個程序或執行緒。
程序(有時候也稱做任務)是指一個程式執行的例項。在Linux系統中,執行緒就是能並行執行並且與他們的父程序(建立他們的程序)共享同一地址空間(一段記憶體區域)和其他資源的輕量級的程序。
上下文是指某一時間點 CPU 暫存器和程式計數器的內容。暫存器是 CPU 內部的數量較少但是速度很快的記憶體(與之對應的是 CPU 外部相對較慢的 RAM 主記憶體)。暫存器通過對常用值(通常是運算的中間值)的快速訪問來提高計算機程式執行的速度。程式計數器是一個專用的暫存器,用於表明指令序列中 CPU 正在執行的位置,存的值為正在執行的指令的位置或者下一個將要被執行的指令的位置,具體依賴於特定的系統。
稍微詳細描述一下,上下文切換可以認為是核心(作業系統的核心)在 CPU 上對於程序(包括執行緒)進行以下的活動:(1)掛起一個程序,將這個程序在 CPU 中的狀態(上下文)儲存於記憶體中的某處,(2)在記憶體中檢索下一個程序的上下文並將其在 CPU 的暫存器中恢復,(3)跳轉到程式計數器所指向的位置(即跳轉到程序被中斷時的程式碼行),以恢復該程序。
上下文切換有時被描述為核心掛起 CPU 當前執行的程序,然後繼續執行之前掛起的眾多程序中的某一個。儘管這麼說對於澄清概念有所幫助,但是這句話本身可能有一點令人困惑。因為通過定義可以知道,程序是指一個程式執行的例項。所以說成掛起一個程序的執行可能更適合一些。
上下文切換與模式切換
上下文切換隻能發生在核心態中。核心態是 CPU 的一種有特權的模式,在這種模式下只有核心執行並且可以訪問所有記憶體和其他系統資源。其他的程式,如應用程式,在最開始都是執行在使用者態,但是他們能通過系統呼叫來執行部分核心的程式碼。系統呼叫在類 Unix 系統中是指活躍的程序(正在執行在 CPU 上的程序)對於核心所提供的服務的請求,例如輸入/輸出(I/O)和程序建立(建立一個新的程序)。I/O 可以被定義為任何資訊流入或流出 CPU 與主記憶體(RAM)。也就是說,一臺電腦的 CPU和記憶體與該電腦的使用者(通過鍵盤或滑鼠)、儲存裝置(硬碟或磁碟驅動)還有其他電腦的任何交流都是 I/O。
這兩種模式(使用者態和核心態)在類 Unix 系統中共存意味著當系統呼叫發生時 CPU 切換到核心態是必要的。這應該叫做模式切換而不是上下文切換,因為沒有改變當前的程序。
上下文切換在多工作業系統中是一個必須的特性。多工作業系統是指多個程序執行在一個 CPU 中互不打擾,看起來像同時執行一樣。這個並行的錯覺是由於上下文在高速的切換(每秒幾十上百次)。當某一程序自願放棄它的 CPU 時間或者系統分配的時間片用完時,就會發生上下文切換。
上下文切換有時也因硬體中斷而觸發。硬體中斷是指硬體裝置(如鍵盤、滑鼠、除錯解調器、系統時鐘)給核心傳送的一個訊號,該訊號表示一個事件(如按鍵、滑鼠移動、從網路連線接收到資料)發生了。
英特爾的 80386 和更高階的 CPU 都支援硬體上下文切換。然而,大多數現代的作業系統通過軟體實現上下文切換,而非使用硬體上下文切換,這樣能夠執行在任何 CPU 上。同時,使用軟體上下文切換可以嘗試獲得更好的效能。軟體的上下文切換最先在 Linux 2.4 中實現。
軟體上下文切換號稱的一個主要優點是,硬體的機制儲存了幾乎所有 CPU 的狀態,軟體能夠有選擇性的儲存需要被儲存的部分並重新載入。然而這個行為對於提升上下文切換的效能到底有多重要,還有一點疑問。其擁護者還宣稱,軟體上下文切換有提高切換程式碼的可能性,它有助於提高正在載入的資料的有效性,從而進一步提高效能。
上下文切換的消耗
上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味著消耗大量的 CPU 時間,事實上,可能是作業系統中時間消耗最大的操作。
Linux相比與其他作業系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。
3、什麼是程序和執行緒?
①、程序是程式執行資源分配的最小單位。程序內部有多個執行緒,會共享這個程序中的資源。
②、執行緒是CPU排程的最小單位。必須依賴程序而存在。
4、並行和併發的區別?
①、並行:同一時刻,可同時處理問題的能力。比如售票視窗多開,多開的視窗表示同時處理的能力。
②、併發:與單位時間相關,在單位時間內處理事情的能力。
5、高併發程式設計的意義、好處和注意事項
①好處:充分利用CPU資源,加快使用者響應時間,程式模組化,非同步化。
②問題:A、執行緒共享資源,存在衝突;B、容易導致死鎖;C、啟用太多執行緒,可能會搞垮機器。
6、學習多執行緒
Java實現多執行緒有三種方式:
①、繼承Thread類
②、實現Runnable介面(無返回值)
③、實現Callable介面(有返回值)
①、Java裡的程式天生就是多執行緒的。
A、ThreadMXBean是Java虛擬機器為我們提供執行緒管理的介面,通過該類可以拿到應用中有多少個執行緒。
B、至少會執行5個執行緒:
1、main主函式執行緒
2、Reference Handler負責清除引用的執行緒
3、Finalizer呼叫物件的Final方法的執行緒
4、Signal Dispatcher分發,處理髮送給虛擬機器訊號的執行緒
5、Attach Listener獲取當前程式執行相關資訊
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class OnlyMain {
public static void main(String[] args) {
// Java虛擬機器 為我們提供的執行緒裡面,執行緒管理的介面,通過該類可以拿到應用程式裡面有多少個執行緒
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 是否看鎖,這裡不看鎖,一般用不到,返回值是ThreadInfo的陣列
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
// 遍歷陣列
for (ThreadInfo threadInfo : threadInfos) {
/**
* main主函式執行緒
* Reference Handler負責清除引用的執行緒
* Finalizer呼叫物件的Final方法的執行緒
* Signal Dispatcher分發,處理髮送給虛擬機器訊號的執行緒
* Attach Listener獲取當前程式執行相關資訊
*/
System.out.println("【" + threadInfo.getThreadId() + "】 " + threadInfo.getThreadName());
}
}
}
控制檯輸出:
【5】 Attach Listener
【4】 Signal Dispatcher
【3】 Finalizer
【2】 Reference Handler
【1】 main
C、實現多執行緒的三種方式及區別:
1、繼承Thread類
2、實現Runnable介面
3、實現Callable介面,允許有返回值,並且不能直接用new Thread來啟動該多執行緒介面物件,而是需要先用FutureTask物件來轉換一次,再呼叫new Thread()來啟動Callable多執行緒物件
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class NewThread {
/* 擴充套件自Thread類 */
/* 實現Runnable介面 */
private static class UseRunnable implements Runnable {
@Override
public void run() {
System.out.println("It is a Runnable!");
}
}
/* 實現Callable介面,允許有返回值 */
private static class UseCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("It is a Callable!");
return "CallableResult";
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 例項化Runnable介面物件
UseRunnable useRunnable = new UseRunnable();
// 通過new Thread來執行Runnable多執行緒物件
new Thread(useRunnable).start();
// 例項化Callable介面物件
UseCallable useCallble = new UseCallable();
// Thread不能夠執行Callable介面物件,只能通過FutureTask介面轉換後在執行
FutureTask<String> futureTask = new FutureTask<>(useCallble);
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
控制檯輸出:
It is a Runnable!
It is a Callable!
CallableResult
理解中斷
如何安全的終止執行緒
1,理解中斷
執行緒自然終止:自然執行完 或 丟擲未處理異常
stop()、resume()、suspend() 三個方法已經在後續的jdk版本已過時,不建議使用
stop()方法:會導致執行緒不正確釋放資源;
suspend()方法:掛起,容易導致死鎖
Java執行緒是協作式工作,而非搶佔式工作;
介紹三種中斷方式:
①、interrupt()方法
interrupt()方法中斷一個執行緒,並不是強制關閉該執行緒,只是跟該執行緒打個招呼,將執行緒的中斷標誌位置為true,執行緒是否中斷,由執行緒本身決定;
②、inInterrupted()方法
inInterrupted()方法判斷當前執行緒是否處於中斷狀態;
③、static 方法interrupted()方法
static方法interrupted()方法判斷當前執行緒是否處於中斷狀態,並將中斷標誌位改為false;
注:方法裡如果丟擲InterruptedException,執行緒的中斷標誌位會被置為false,如果確實需要中斷執行緒,則需要在catch裡面再次呼叫interrupt()方法
public class HasInterruptException {
// 定義一個私有的Thread整合類
private static class UseThread extends Thread {
@Override
public void run() {
// 獲取當前執行緒名字
String threadName = Thread.currentThread().getName();
// 判斷執行緒是否處於中斷標誌位
while (!isInterrupted()) {
// 測試用interrupt中斷後,報InterruptedException時狀態變化
try {
System.out.println(threadName + "is run !!!!");
// 設定休眠毫秒數
Thread.sleep(3000);
} catch (InterruptedException e) {
// 判斷中斷後丟擲InterruptedException後中斷標誌位的狀態
System.out.println(threadName + " interrupt flag is " + isInterrupted());
// 如果丟擲InterruptedException後中斷標誌位置為了false,則需要手動再呼叫interrupt()方法,如果不呼叫,則中斷標誌位為false,則會一直在死迴圈中而不會退出
interrupt();
e.printStackTrace();
}
// 打印出執行緒名稱
System.out.println(threadName);
}
// 檢視當前執行緒中斷標誌位的狀態
System.out.println(threadName + " interrupt flag is " + isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
UseThread useThread = new UseThread();
useThread.setName("HasInterruptException--");
useThread.start();
Thread.sleep(800);
useThread.interrupt();
}
}
控制檯輸出結果:
HasInterruptException--is run !!!!
HasInterruptException-- interrupt flag is false
java.lang.InterruptedException: sleep interrupted
HasInterruptException--
HasInterruptException-- interrupt flag is true
at java.lang.Thread.sleep(Native Method)
at com.xiangxue.ch1.safeend.HasInterruptException$UseThread.run(HasInterruptException.java:18)
執行緒基礎、執行緒之間的共享和協作
執行緒常用方法和執行緒的狀態
start():呼叫start()方法後,使執行緒從新建狀態處於就緒狀態。
sleep():呼叫sleep()方法後,設定休眠時間,使執行緒從執行狀態處於阻塞(休眠)狀態,休眠時間到,執行緒從阻塞狀態轉變為就緒狀態。
wait():呼叫wait()方法後,使執行緒從執行狀態處於阻塞(休眠)狀態,只有通過notify()或者notifyAll()方法重新使執行緒處於就緒狀態。
notify():當執行緒呼叫wait方法後,進入阻塞狀態,如果有多個執行緒阻塞,呼叫notify()方法後,CPU會呼叫處於執行緒棧上的第一個阻塞執行緒,並將執行緒阻塞狀態置為就緒狀態。只啟用一個阻塞執行緒。具體看下面介紹和實現。
notifyAll():當執行緒呼叫wait方法後,進入阻塞狀態,如果有多個執行緒阻塞,呼叫notifyAll()方法後,CPU會將所有處於執行緒棧上的所有呼叫了wait()方法阻塞的執行緒進行依次執行,直到所有執行緒執行完或丟擲異常。具體看下面介紹和實現。
interrupt():呼叫interrupt()方法後,不是強制關閉執行緒,只是跟執行緒打個招呼,將執行緒的中斷標誌位置為true,執行緒是否中斷,由執行緒本身決定。
isInterrypt():執行緒中斷標誌位,true/false兩個Boolean值,用來判斷是否呼叫interrupt()方法,告訴執行緒是否中斷。
interrupted():判斷執行緒是否處於中斷狀態,並將中斷標誌位改為false。
run():執行執行緒的方法。
join():thread.Join把指定的執行緒加入到當前執行緒,可以將兩個交替執行的執行緒合併為順序執行的執行緒。比如線上程B中呼叫了執行緒A的Join()方法,直到執行緒A執行完畢後,才會繼續執行執行緒B。
yield():Thread.yield()方法作用是:暫停當前正在執行的執行緒物件,並執行其他執行緒。yield()應該做的是讓當前執行執行緒回到可執行狀態,以允許具有相同優先順序的其他執行緒獲得執行機會。因此,使用yield()的目的是讓相同優先順序的執行緒之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因為讓步的執行緒還有可能被執行緒排程程式再次選中。結論:yield()從未導致執行緒轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致執行緒從執行狀態轉到可執行狀態,但有可能沒有效果。
synchronized內建鎖
1、用處
synchronized作為執行緒同步的關鍵字,設計到鎖的概念,下面就對鎖的概念進行詳細介紹。
Java內建鎖是一個互斥鎖,這就說明最多隻有一個執行緒能夠獲得該鎖,例如兩個執行緒:執行緒A和執行緒B,如果執行緒A嘗試去獲得執行緒B的內建鎖,則執行緒A必須等待或者阻塞,直到執行緒B釋放這個鎖為止;如果執行緒B永不釋放這個鎖,則執行緒A則永遠處於等待或阻塞狀態。使用內建鎖是執行緒安全的。
Java的物件鎖和類鎖在鎖的概念上,與內建鎖幾乎是一致的,但是物件鎖和類鎖的區別是非常大的。
2、物件鎖
用synchronized修飾非靜態方法、用synchronized(this)作為同步程式碼塊、用synchronized(非this物件)的用法鎖的是物件,執行緒想要執行對應的同步程式碼,需要先獲得物件鎖。
3、類鎖
用synchronized修飾靜態方法、用synchronized(類.class)的用法鎖的是類,執行緒想要執行對應的同步程式碼,需要先獲得類鎖。
對synchronized關鍵字的用法,物件鎖,類鎖的使用參考之前總結的文章實現案例:
總結:靜態方法一定會同步,非靜態方法需在單例模式才生效,但是也不能都用靜態同步方法,總之用得不好可能會給效能帶來極大的影響。另外,有必要說一下的是Spring的bean預設是單例的。
物件鎖:鎖的是類的物件例項。
類鎖 :鎖的是每個類的的Class物件,每個類的的Class物件在一個虛擬機器中只有一個,所以類鎖也只有一個。
等待和通知
一、應用場景:
一個執行緒修改了一個值,另一個執行緒感受到了值的變化,進行相應的操作。前一個執行緒類比於一個生產者,後一個執行緒是消費者。如何讓消費者感受到生產者的一個值的變化呢?
解決方案一:
輪詢:每隔一分鐘就去輪詢一次,總有一個時間點能夠獲取到生產者的變換。比如煲湯,每個一分鐘就去看一下是否煲好了。結果:這樣會很累,很佔用資源。
輪詢的缺點:很難確保一個及時性,每隔一段時間就要去操作一次,資源開銷很大,做很多無用功。
解決方案二:
等待和通知機制方式:當一個執行緒呼叫了wait()方法,會進入一個等待狀態,而另外一個執行緒對值進行操作後,呼叫notify()或者notifyAll()方法後,通知第一個執行緒去操作某件事情。注意:wait()、notify()/notifyAll()是物件上的方法。
wait()等待方會怎麼做?
1、獲取物件的鎖;一定是在迴圈裡面去操作;
2、迴圈裡判斷是否滿足條件,不滿足條件呼叫wait()方法,一直等待;
3、滿足條件,執行業務邏輯;
notify()、notifyAll()會怎麼做?
1、依然要獲取物件的鎖;
2、改變相關條件;
3、通知所有等待在物件的執行緒
以上介紹了wait、notify/notifyAll的標準正規化。
三、notify()和notifyAll()區別:
應該儘量應用notifyAll(),使用notify()的話,jvm會執行已經加入等待執行緒棧裡面的第一個執行緒,給我們一種感觀就是隨機的選擇了一種執行緒,如果該執行緒達到條件就正好執行那一條,其實這是一個誤區,而是jvm會選擇線上程棧裡面的第一個執行緒。因此如果用notify()的話,可能會造成訊號丟失的情況。
關於wait()、notify()、notifyAll()的用法看之前的文章實現例項:
等待通知wait()、notify()、notifyAll()原理和介紹
join()方法
執行緒A,執行了執行緒B的join方法,執行緒A必須要等待B執行完成了以後,執行緒A才能繼續自己的工作
呼叫yield() 、sleep()、wait()、notify()等方法對鎖有何影響?
執行緒在執行yield()以後,持有的鎖是不釋放的
sleep()方法被呼叫以後,持有的鎖是不釋放的
調動方法之前,必須要持有鎖。呼叫了wait()方法以後,鎖就會被釋放,當wait方法返回的時候,執行緒會重新持有鎖
調動方法之前,必須要持有鎖,呼叫notify()方法本身不會釋放鎖的
ForkJoinPool
用途,概念:
ForkJoinPool的優勢在於,利用多核CPU,將一個任務,拆分成多個小任務 ,將這些小任務分配到多個處理器上並行執行;當小任務都執行完成之後,再將結果進行合併彙總。每個小任務間都沒有關聯,與原任務的形式相同。體現了“分而治之”的概念。任務遞迴分配成若干個小任務 -- 並行求值 -- 結果合併。
1、ForkJoinPool
Java7提供了ForkJoinPool來支援將一個任務拆分成多個小任務進行平行計算,再將多個“小任務”的結果進行join彙總。
2、invoke、invokeAll
執行指定的任務,等待任務,完成任務返回結果。
3、遞迴演算法
在繼承RecurisiveTask(有返回結果),RecurisiveAction(無返回結果)類時,通過遞迴的方式,來將任務拆分成一個一個的小任務,通過invokeAll()方法來排程子任務,等待任務完成返回結果。注意,只有在ForkJoinPool執行計算過程中呼叫它們。
關於ForkJoin的實現案例看之前文章:
ForkJoinPool非同步和同步的區別:
1,呼叫execute()方法時,為非同步執行
2,呼叫invoke()方法時,為同步執行
invokeAll()方法
1,invokeAll()方法, 我們檢視原始碼的實現可以看出,返回一個集合形式的結果,因此可以在for迴圈的泛型裡面直接用invokeAll(集合物件)方法
2、呼叫了invokeAll()方法後,將所有迴圈內的子方法都join()起來,等待子任務的完成
CountDownLatch、CyclicBarrier
一、CountDownLatch
官方介紹:
CountDownLatch是在java1.5被引入的,它存在於java.util.concurrent包下。CountDownLatch這個類能夠使一個執行緒等待其他執行緒完成各自的工作後再執行。例如,應用程式的主執行緒希望在負責啟動框架服務的執行緒已經啟動所有的框架服務之後再執行。
CountDownLatch是通過一個計數器來實現的,計數器的初始值為執行緒的數量。每當一個執行緒完成了自己的任務後,計數器的值就會減1。當計數器值到達0時,它表示所有的執行緒已經完成了任務,然後在閉鎖上等待的執行緒就可以恢復執行任務。
什麼意思呢?就是執行過程中,有幾個執行緒,那麼只有當幾個執行緒同時就緒之後,類似於100米賽跑一樣,必須所有運動員到達起跑線之後,通過一個發令槍才能夠一起衝向終點,執行起來。
裡面就有幾個方法,分別介紹一下:
countDown()方法:該方法初始值設定為允許執行的執行緒數,這裡比如賽道上只能容納10個人,則初始值為10,然後每一次執行緒執行完,則將初始值10減1,一直減到0為止,然後表示所有的運動員都就位了,然後就等待發令槍聲響就開始同時運行了。這裡要注意的點是,必須在每一個執行緒執行完之後,呼叫countDown()方法,否則資料將出錯!
await()方法:該方法就相當於發令槍,當判斷countDown()將初始值一直減到0 之後,表示所有的執行緒已經就緒了,就執行await()方法,所有執行緒就開始同時執行後續操作。
未整理完,今晚先到這裡。明天繼續整理
以上就是多執行緒和執行緒併發工具總結
歡迎關注部落格,後續會持續的進行總結和階段性的整合總結。希望能夠和更多人探討和指正。