從0開始深入了解并發、線程與等待通知機制(上)
程序和線程
程序:是作業系統進行資源配置設定的最小機關。一個程序是一個程式的一次執行過程。每啟動一個程序,作業系統就會為它配置設定一塊獨立的記憶體空間,用于存儲PCB、資料段、程式段等資源。每個程序占有一塊獨立的記憶體空間。
在作業系統沒有引入程序之前,由于CPU一次隻能執行一個程式,是以多個程式隻能順序執行,而CPU的速度很快,磁盤、網路等IO的速度很慢,造成CPU會有大量空閑的時間,此時CPU的使用率很低,為了解決CPU的使用率低的問題,作業系統引入了程序以及中斷處理,實作了在同一時間段内,多個程式的并發執行,這個程式執行一點,那個程式執行一點,這樣并發交替的執行大大提高了CPU的使用率。
線程:線程是程序的一個實體,是CPU排程和分派的基本機關,它是比程序更小的能獨立運作的基本機關。線程自己基本上不擁有系統資源,隻擁有一點在運作中必不可少的資源(如程式計數器,一組寄存器和棧),但是它可與同屬一個程序的其他的線程共享程序所擁有的全部資源。
面試題:程序之間的通信?
1:管道,有匿名管道和命名管道。
當一個程序fork出一個子程序的時候,這時候雙方都知道對方存在,具有親緣關系的兩個程序的通信,可以用匿名管道。
當兩個程序沒有親緣關系,就應該用命名管道。
2:信号,信号分發,用于通知程序有某事發生。
3:消息隊列,就和我們平常用mq差不多,建立一個記憶體隊列,其他程序往這個記憶體隊列發送消息。
4:共享記憶體,多個程序通路同一塊記憶體空間。 這種方式需要依賴某種操作,比互斥和信号等。
5:信号量,為了防止出現因多個程式同時通路一個共享資源而引發的一系列問題,我們需要一種方法,它可以通過生成并使用令牌來授權,在任一時刻隻能有一個執行線程通路代碼的臨界區域。臨界區域是指執行資料更新的代碼需要獨占式地執行。而信号量就可以提供這樣的一種通路機制,讓一個臨界區同一時間隻有一個線程在通路它,也就是說信号量是用來調協程序對共享資源的通路的。信号量是一個特殊的變量,程式對其通路都是原子操作,且隻允許對它進行等待(即P(信号變量))和發送(即V(信号變量))資訊操作。最簡單的信号量是隻能取0和1的變量,這也是信号量最常見的一種形式,叫做二進制信号量。而可以取多個正整數的信号量被稱為通用信号量。這裡主要讨論二進制信号量。
6:套接字:socket通信。
CPU核心數和線程數的關系
同一時刻,一個cpu隻能運作一個線程。cpu核心和線程同時運作的關系是1:1,inter引入超線程技術後,核心和cpu的關系是1:2,那麼在設定最優線程數的時候應該是cpu*2。避免cpu不停的上下文切換,也就是我們平常用到最多的設定方式:
Runtime.getRuntime().availableProcessors()*2
上下文切換
作業系統将線程或者程序從cpu排程出去的時候,會把目前線程、程序的cache資料儲存出去,就比如說CPU寄存器和程式計數器。這個過程叫做上下文切換。它的代價是相對比較大的。引發上下文切換有線程切換,程序切換,系統調用… …。對一個簡單的指令cpu處理,需要大概幾個或者幾十個時鐘周期。上下文切換大概需要5000 - 20000個始終周期。synchronized是有鎖操作,cas是無鎖操作。cas一般來說是比synchronized性能好點,但是不停的cas上下文切換也會浪費性能,導緻比synchronized更差。
這裡的CPU寄存器包括:
程式記數寄存器:跟蹤程式執行的準确位置
堆棧指針寄存器:訓示操作棧項
架構寄存器:指向目前執行的環境
變量寄存器:指向目前執行環境中第一個本地變量
并行和并發
并發:指應用能夠交替執行不同的任務,比如單個cpu下多線程執行并不是同時執行,而是不停的上下文切換這兩個任務,以達到‘同時‘執行的效果,隻是切換的較快肉眼感覺不到。
并行:指應用能同時執行不同的任務。比如說兩個線程分别由兩個不桶的cpu去執行,就叫做并行。
建立線程的幾種方式
建立線程:
從底層代碼上看,隻有一種,當隻有調用Thread類的native start0方法核心建立線程然後核心傳回來調用run方法。start()->JVM_StartThread -> new JavaThread->os::start_thread -> run()
從java源碼注釋上說有兩種,一種是new Thread另一種是實作Runnable方法(Runnable底層也是Thread start0建立的線程)
從應用程式,根據不同的需求派生出大概有五種(也有可能多種)方式如下
方式一:繼承于Thread類
方式二:實作Runnable接口
方式三:實作Callable接口,Future,RunnableFuture
方式四:使用線程池
方式五:使用匿名類
Thread和Runnable的差別:
Thread才是Java裡對線程的唯一抽象。Runnable是對任務的。Thread可以接受任意一個Runnable執行個體并執行
方式一:繼承Thread類
image-20230227165501497
static class ThreadTest extends Thread{
@Override
public void run(){
System.out.println("繼承Thread。"+currentThread().getName()+":正在運作。");
}
}
方式二:實作Runnable接口
static class RunnableTest implements Runnable{
@Override
public void run() {
System.out.println("實作Runnable。"+Thread.currentThread().getName()+":正在運作。");
}
}
方式一和方式二都是沒有傳回值的。callable是有傳回值的
方式三:實作callable接口
static class CallableTest implements Callable {
@Override
public Object call() throws Exception {
System.out.println("實作callable。" + Thread.currentThread().getName() + ":正在運作");
return "随便傳回了";
}
}
public static void main(String[] args) throws Exception {
CallableTest callableTest = new CallableTest();
FutureTask futureTask = new FutureTask(callableTest);
new Thread(futureTask).start();
Object o = futureTask.get();
}
其實callable底層也是實作了Runnable接口,隻不過它callable寫了具體的邏輯方法是call(),将call()方法傳遞給了FutureTask類中,FutureTask類中儲存着call方法和實作了Runnable的run方法,還有線程的狀态和傳回值變量。
當調用futureTask.get()的時候判斷線程是否執行完成并且傳回它自己儲存的傳回值變量。
如果沒有執行完成就調用park。線程的執行是調用了FutureTask實作的run方法,run方法裡邊調用了call方法,在finally裡邊調用unpark,也就是把調用get方法的線程unpark掉,并傳回值。
方式四:
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new ThreadTest());
service.shutdown();
方式五:
其實就是8的特性
Thread thread = new Thread(()->{
System.out.println(111);
});
綜上案例真正建立線程還是Thread.run方法。其他的都是衍生品。
如何安全的終止線程
1:代碼執行完成
2:抛出異常
3:stop(廢棄的,但是他不會釋放任何資源,比如IO,網卡,鎖… 這些資源不會被釋放,占着茅坑不拉屎,當我們在寫檔案時候,正确打開了io,調用stop後沒有調用IO的結束符,導緻檔案損壞),
suspend挂起線程(cpu不在執行,隻有當有人喚醒它才會繼續執行,和stop一樣都不會釋放資源)
4:最好的方式是中斷,中斷信号。調用線程的interrupt,interrupt是jvm中線程類的一個變量。
當其他線程調用線程A的interrupt的時候隻是将線程A這個變量改為true,然後線程A判斷isInterrupt判斷是否有中斷信号,線程A也可以不理會,看心情。
Thread類中interrupt()、interrupted()和isInterrupted()方法介紹
interrupt();将線程狀态設定成true
/**
* 中斷此線程。
* <p>線程可以中斷自身,這是允許的。在這種情況下,不用進行安全性驗證({@link #checkAccess() checkAccess} 方法檢測)
* <p>若目前線程由于 wait() 方法阻塞,或者由于join()、sleep()方法,然後線程的中斷狀态将被清除,并且将收到 {@link InterruptedException}。
* <p>如果線程由于 IO操作({@link java.nio.channels.InterruptibleChannel InterruptibleChannel})阻塞,那麼通道 channel 将會關閉,
* 并且線程的中斷狀态将被設定,線程将收到一個 {@link java.nio.channels.ClosedByInterruptException} 異常。
* <p>如果線程由于在 {@link java.nio.channels.Selector} 中而阻塞,那麼線程的中斷狀态将會被設定,它将立即從選擇操作中傳回。
*該值可能是一個非零值,就像調用選擇器的{@link java.nio.channels.Selector#wakeupakeup}方法一樣。
*
* <p>如果上述條件均不成立,則将設定該線程的中斷狀态。</p>
* <p>中斷未運作的線程不必産生任何作用。
* @throws SecurityException 如果目前線程無法修改此線程
*/
public void interrupt() {
//如果調用中斷的是線程自身,則不需要進行安全性判斷
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // 隻是設定中斷标志
b.interrupt(this);
return;
}
}
interrupt0();
}
interrupted(); //将線程歸為,設定為false
isInterrupted()//傳回檔期那線程的狀态
優雅的通過線程信号中斷線程:
就是線程A不斷的while判斷isInterupted()然後做出相應。最後還得interrupted(),将線程歸為。
那麼我可以用自定義的變量來代替Interrupt?
不建議!
1:Interrupt是jvm源碼裡邊Thread類裡的一個變量。是每個線程獨有的。
2:當我們調用Thread.sleep,阻塞隊列中take,pull,future.get… …這些方法是會阻塞的,是被cpu給挂起來了的。那我們自定義的變量是無法中斷這些阻塞的。就比如這些方法都會抛出一個中斷異常,InterruptExcetion所有的阻塞類的都會抛出這個異常,當阻塞的時候,被别的線程調用interrupt的時候會抛出這個異常,可以快速的相應。自定義的不能快速相應隻有當不阻塞的時候才會相應。
需要補充sleep的時候抛出Interrupt
當執行兩次start的時候會怎樣?
在調用start方法的時候線程的狀态會變化,在執行start的時候會判斷這個狀态。
當我們new Thread的時候隻是在JVM堆裡邊建立一個記憶體變量。隻有在調用start()->start0()方法的時候才會真正的和作業系統的線程挂上鈎,才會真正的建立和運作線程,然後這個線程才會執行我們的run方法,start0()->JVM_StartThread -> new JavaThread->os::start_thread -> run()。