很多同學面對多線程的問題都很頭大,因為自己做項目很難用到,但是但凡高薪的職位面試都會問到。。畢竟現在大廠里用的都是多線程高並發,所以這塊內容不吃透肯定是不行的。
今天這篇文章,作為多線程的基礎篇,先來談談以下問題:
-
為什么要用多線程? -
程序 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 點,自習室里我們不見不散 ❤️