很多同學面對多執行緒的問題都很頭大,因為自己做專案很難用到,但是但凡高薪的職位面試都會問到。。畢竟現在大廠裡用的都是多執行緒高併發,所以這塊內容不吃透肯定是不行的。
今天這篇文章,作為多執行緒的基礎篇,先來談談以下問題:
為什麼要用多執行緒? 程式 vs 程式 vs 執行緒 建立執行緒的 4 種方式?
為什麼要用多執行緒
任何一項技術的出現都是為了解決現有問題。
之前的網際網路大多是單機服務,體量小;而現在的更多是叢集服務,同一時刻有多個使用者同時訪問伺服器,那麼會有很多執行緒併發訪問。
比如在電商系統裡,同一時刻比如整點搶購時,大量使用者同時訪問伺服器,所以現在公司裡開發的基本都是多執行緒的。
使用多執行緒確實提高了執行的效率,但與此同時,我們也需要特別注意資料的增刪改情況,這就是執行緒安全問題,比如之前說過的 HashMap vs HashTable
,Vector vs ArrayList
。
要保證執行緒安全也有很多方式,比如說加鎖,但又可能會出現其他問題比如死鎖,所以多執行緒相關問題會比較麻煩。
因此,我們需要理解多執行緒的原理和它可能會產生的問題以及如何解決問題,才能拿下高薪職位。
程式 vs 執行緒
程式 program
說到程式,就不得不先說說程式。
程式,說白了就是程式碼,或者說是一系列指令的集合。比如「微信.exe」這就是一個程式,這個檔案最終是要拿到 CPU 裡面去執行的。
程式 process
當程式執行起來,它就是一個程式。
所以程式是“死”的,程式是“活”的。
比如在工作管理員裡的就是一個個程式,就是“動起來”的應用程式。
Q:這些程式是並行執行的嗎?
單核 CPU 一個時間片裡只能執行一個程式。但是因為它切換速度很快,所以我們感受不到,就造成了一種多程式的假象。(多核 CPU 那真的就是並行執行的了。)
Q:那如果這個程式沒執行完呢?
當程式 A 執行完一個時間片,但是還沒執行完時,為了方便下次接著執行,要儲存剛剛執行完的這些資料資訊,叫做「儲存現場」。
然後等下次再搶到了資源執行的時候,先「恢復現場」,再開始繼續執行。
這樣迴圈往復。。
這樣反覆的儲存啊、恢復啊,都是額外的開銷,也會讓程式執行變慢。
Q:有沒有更高效的方式呢?
如果兩個執行緒歸屬同一個程式,就不需要儲存、恢復現場了。
這就是 NIO 模型的思路,也是 NIO 模型比 BIO 模型效率高很多的原因,我們之後再講。
執行緒 thread
執行緒,是一個程式裡的具體的執行路徑,就是真正幹活的。
在一個程式裡,一個時間片也只能有一個執行緒在執行,但因為時間片的切換速度非常快,所以看起來就好像是同時進行的。
一個程式裡至少有一個執行緒。比如主執行緒,就是我們平時寫的 main()
函式,是使用者執行緒;還有 gc
執行緒是 JVM 生產的,負責垃圾回收,是守護執行緒。
每個執行緒有自己的棧 stack
,記錄該執行緒裡面的方法相互呼叫的關係;
但是一個程式裡的所有執行緒是共用堆 heap
的。
那麼不同的程式之間是不可以互相訪問記憶體的,每個程式有自己的記憶體空間 memeory space
,也就是虛擬記憶體 virtual memory
。
通過這個虛擬記憶體,每一個程式都感覺自己擁有了整個記憶體空間。
虛擬記憶體的機制,就是遮蔽了實體記憶體的限制。
Q:那如果實體記憶體被用完了呢?
用硬碟,比如 windows 系統的分頁檔案,就是把一部分虛擬記憶體放到了硬碟上。
相應的,此時程式執行會很慢,因為硬碟的讀寫速度比記憶體慢很多,是我們可以感受到的慢,這就是為什麼開多了程式電腦就會變卡的原因。
Q:那這個虛擬記憶體是有多大呢?
對於 64 位作業系統來說,每個程式可以用 64 個二進位制位,也就是 2^64
這麼大的空間!
如果還不清楚二進位制相關內容的,公眾號內回覆「二進位制」獲取相應的文章哦~
總結
總結一下,在一個時間片裡,一個 CPU 只能執行一個程式。
CPU 給某個程式分配資源後,這個程式開始執行;程式裡的執行緒去搶佔資源,一個時間片就只有一個執行緒能執行,誰先搶到就是誰的。
多程式 vs 多執行緒
每個程式是獨立的,程式 A 出問題不會影響到程式 B;
雖然執行緒也是獨立執行的,但是一個程式裡的執行緒是共用同一個堆,如果某個執行緒 out of memory
,那麼這個程式裡所有的執行緒都完了。
所以多程式能夠提高系統的容錯性 fault tolerance
,而多執行緒最大的好處就是執行緒間的通訊非常方便。
程式之間的通訊需要藉助額外的機制,比如程式間通訊 interprocess communication
- IPC
,或者網路傳遞等等。
如何建立執行緒
上面說了一堆概念,接下來我們看具體實現。
Java 中是通過 java.lang.Thread
這個類來實現多執行緒的功能的,那我們先來看看這個類。
從文件中我們可以看到,Thread
類是直接繼承 Object
的,同時它也是實現了 Runnable
介面。
官方文件裡也寫明瞭 2 種建立執行緒的方式:
一種方式是從 Thread
類繼承,並重寫 run()
,run()
方法裡寫的是這個執行緒要執行的程式碼;
啟動時通過 new
這個 class
的一個例項,呼叫 start()
方法啟動執行緒。
二是實現 Runnable
介面,並實現 run()
,run()
方法裡同樣也寫的是這個執行緒要執行的程式碼;
稍有不同的是啟動執行緒,需要 new
一個執行緒,並把剛剛建立的這個實現了 Runnable
介面的類的例項傳進去,再呼叫 start()
,這其實是代理模式。
如果面試官問你,還有沒有其他的,那還可以說:
實現
Callable
介面;通過執行緒池來啟動一個執行緒。
但其實,用執行緒池來啟動執行緒時也是用的前兩種方式之一建立的。
這兩種方式在這裡就不細說啦,我們具體來看前兩種方式。
繼承 Thread 類
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("小齊666:" + i);
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for (int i = 0; i < 100; i++) {
System.out.println("主執行緒" + i + ":齊姐666");
}
}
}
在這裡,
main
函式是主執行緒,是程式的入口,執行整個程式;程式開始執行後先啟動了一個新的執行緒
myThread
,在這個執行緒裡輸出“小齊”;主執行緒並行執行,並輸出“主執行緒i:齊姐”。
來看下結果,就是兩個執行緒交替誇我嘛~
Q:為啥和我執行的結果不一樣?
多執行緒中,每次執行的結果可能都會不一樣,因為我們無法人為控制哪條執行緒在什麼時刻先搶到資源。
當然了,我們可以給執行緒加上優先順序 priority
,但高優先順序也無法保證這條執行緒一定能先被執行,只能說有更大的概率搶到資源先執行。
實現 Runnable 介面
這種方式用的更多。
public class MyRunnable implements Runnable {
@Override
public void run() {
for(int i = 0; i < 100; i++) {
System.out.println("小齊666:" + i);
}
}
public static void main(String[] args) {
new Thread(new MyRunnable()).start();
for(int i = 0; i < 100; i++) {
System.out.println("主執行緒" + i + ":齊姐666");
}
}
}
結果也差不多:
像前文所說,這裡執行緒啟動的方式和剛才的稍有不同,因為新建的的這個類只是實現了 Runnable
介面,所以還需要一個執行緒來“代理”執行它,所以需要把我們新建的這個類的例項傳入到一個執行緒裡,這裡其實是代理模式。這個設計模式之後再細講。
小結
那這兩種方式哪種好呢?
使用 Runnable 介面更好,主要原因是 Java 單繼承。
另外需要注意的是,在啟動執行緒的的時候用的是 start()
,而不是 run()
。
呼叫 run()
僅僅是呼叫了這個方法,是普通的方法呼叫;而 start()
才是啟動執行緒,然後由 JVM 去呼叫該執行緒的 run()
。
好了,以上就是多執行緒第一篇的所有內容了,這裡主要是幫助大家複習一下基礎概念,以及沒有接觸的小夥伴可以入門。想看更多關於多執行緒的內容的話,記得給我點贊留言哦~
我是小齊,終身學習者,每晚 9 點,自習室裡我們不見不散 ❤️