拜托!別再問我多線程的這些問題了
本文轉(zhuǎn)載自微信公眾號「碼農(nóng)田小齊」,作者小齊本齊 。轉(zhuǎn)載本文請聯(lián)系碼農(nóng)田小齊公眾號。
很多同學面對多線程的問題都很頭大,因為自己做項目很難用到,但是但凡高薪的職位面試都會問到。。畢竟現(xiàn)在大廠里用的都是多線程高并發(fā),所以這塊內(nèi)容不吃透肯定是不行的。
今天這篇文章,作為多線程的基礎篇,先來談談以下問題:
- 為什么要用多線程?
- 程序 vs 進程 vs 線程
- 創(chuàng)建線程的 4 種方式?
為什么要用多線程
任何一項技術(shù)的出現(xiàn)都是為了解決現(xiàn)有問題。
之前的互聯(lián)網(wǎng)大多是單機服務,體量小;而現(xiàn)在的更多是集群服務,同一時刻有多個用戶同時訪問服務器,那么會有很多線程并發(fā)訪問。
比如在電商系統(tǒng)里,同一時刻比如整點搶購時,大量用戶同時訪問服務器,所以現(xiàn)在公司里開發(fā)的基本都是多線程的。
使用多線程確實提高了運行的效率,但與此同時,我們也需要特別注意數(shù)據(jù)的增刪改情況,這就是線程安全問題,比如之前說過的 HashMap vs HashTable,Vector vs ArrayList。
要保證線程安全也有很多方式,比如說加鎖,但又可能會出現(xiàn)其他問題比如死鎖,所以多線程相關(guān)問題會比較麻煩。
因此,我們需要理解多線程的原理和它可能會產(chǎn)生的問題以及如何解決問題,才能拿下高薪職位。
進程 vs 線程
程序program
說到進程,就不得不先說說程序。
程序,說白了就是代碼,或者說是一系列指令的集合。比如「微信.exe」這就是一個程序,這個文件最終是要拿到 CPU 里面去執(zhí)行的。
進程 process
當程序運行起來,它就是一個進程。
所以程序是“死”的,進程是“活”的。
比如在任務管理器里的就是一個個進程,就是“動起來”的應用程序。
Q:這些進程是并行執(zhí)行的嗎?
單核 CPU 一個時間片里只能執(zhí)行一個進程。但是因為它切換速度很快,所以我們感受不到,就造成了一種多進程的假象。(多核 CPU 那真的就是并行執(zhí)行的了。)
Q:那如果這個進程沒執(zhí)行完呢?
當進程 A 執(zhí)行完一個時間片,但是還沒執(zhí)行完時,為了方便下次接著執(zhí)行,要保存剛剛執(zhí)行完的這些數(shù)據(jù)信息,叫做「保存現(xiàn)場」。
然后等下次再搶到了資源執(zhí)行的時候,先「恢復現(xiàn)場」,再開始繼續(xù)執(zhí)行。
這樣循環(huán)往復。。
這樣反復的保存啊、恢復啊,都是額外的開銷,也會讓程序執(zhí)行變慢。
Q:有沒有更高效的方式呢?
如果兩個線程歸屬同一個進程,就不需要保存、恢復現(xiàn)場了。
這就是 NIO 模型的思路,也是 NIO 模型比 BIO 模型效率高很多的原因,我們之后再講。
線程 thread
線程,是一個進程里的具體的執(zhí)行路徑,就是真正干活的。
在一個進程里,一個時間片也只能有一個線程在執(zhí)行,但因為時間片的切換速度非??欤钥雌饋砭秃孟袷峭瑫r進行的。
一個進程里至少有一個線程。比如主線程,就是我們平時寫的 main() 函數(shù),是用戶線程;還有 gc 線程是 JVM 生產(chǎn)的,負責垃圾回收,是守護線程。
每個線程有自己的棧 stack,記錄該線程里面的方法相互調(diào)用的關(guān)系;
但是一個進程里的所有線程是共用堆 heap 的。
那么不同的進程之間是不可以互相訪問內(nèi)存的,每個進程有自己的內(nèi)存空間 memeory space,也就是虛擬內(nèi)存 virtual memory。
通過這個虛擬內(nèi)存,每一個進程都感覺自己擁有了整個內(nèi)存空間。
虛擬內(nèi)存的機制,就是屏蔽了物理內(nèi)存的限制。
Q:那如果物理內(nèi)存被用完了呢?
用硬盤,比如 windows 系統(tǒng)的分頁文件,就是把一部分虛擬內(nèi)存放到了硬盤上。
相應的,此時程序運行會很慢,因為硬盤的讀寫速度比內(nèi)存慢很多,是我們可以感受到的慢,這就是為什么開多了程序電腦就會變卡的原因。
Q:那這個虛擬內(nèi)存是有多大呢?
對于 64 位操作系統(tǒng)來說,每個程序可以用 64 個二進制位,也就是 2^64 這么大的空間!
如果還不清楚二進制相關(guān)內(nèi)容的,公眾號內(nèi)回復「二進制」獲取相應的文章哦~
總結(jié)
總結(jié)一下,在一個時間片里,一個 CPU 只能執(zhí)行一個進程。
CPU 給某個進程分配資源后,這個進程開始運行;進程里的線程去搶占資源,一個時間片就只有一個線程能執(zhí)行,誰先搶到就是誰的。
多進程 vs 多線程
每個進程是獨立的,進程 A 出問題不會影響到進程 B;
雖然線程也是獨立運行的,但是一個進程里的線程是共用同一個堆,如果某個線程 out of memory,那么這個進程里所有的線程都完了。
所以多進程能夠提高系統(tǒng)的容錯性 fault tolerance ,而多線程最大的好處就是線程間的通信非常方便。
進程之間的通信需要借助額外的機制,比如進程間通訊 interprocess communication -IPC,或者網(wǎng)絡傳遞等等。
如何創(chuàng)建線程
上面說了一堆概念,接下來我們看具體實現(xiàn)。
Java 中是通過 java.lang.Thread 這個類來實現(xiàn)多線程的功能的,那我們先來看看這個類。
從文檔中我們可以看到,Thread 類是直接繼承 Object 的,同時它也是實現(xiàn)了 Runnable 接口。
官方文檔里也寫明了 2 種創(chuàng)建線程的方式:
一種方式是從 Thread 類繼承,并重寫 run(),run() 方法里寫的是這個線程要執(zhí)行的代碼;
啟動時通過 new 這個 class 的一個實例,調(diào)用 start() 方法啟動線程。
二是實現(xiàn) Runnable 接口,并實現(xiàn) run(),run() 方法里同樣也寫的是這個線程要執(zhí)行的代碼;
稍有不同的是啟動線程,需要 new 一個線程,并把剛剛創(chuàng)建的這個實現(xiàn)了 Runnable 接口的類的實例傳進去,再調(diào)用 start(),這其實是代理模式。
如果面試官問你,還有沒有其他的,那還可以說:
實現(xiàn) Callable 接口;
通過線程池來啟動一個線程。
但其實,用線程池來啟動線程時也是用的前兩種方式之一創(chuàng)建的。
這兩種方式在這里就不細說啦,我們具體來看前兩種方式。
繼承 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 函數(shù)是主線程,是程序的入口,執(zhí)行整個程序;
- 程序開始執(zhí)行后先啟動了一個新的線程 myThread,在這個線程里輸出“小齊”;
- 主線程并行執(zhí)行,并輸出“主線程i:齊姐”。
來看下結(jié)果,就是兩個線程交替夸我嘛~
Q:為啥和我運行的結(jié)果不一樣?
多線程中,每次運行的結(jié)果可能都會不一樣,因為我們無法人為控制哪條線程在什么時刻先搶到資源。
當然了,我們可以給線程加上優(yōu)先級 priority,但高優(yōu)先級也無法保證這條線程一定能先被執(zhí)行,只能說有更大的概率搶到資源先執(zhí)行。
實現(xiàn) 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");
- }
- }
- }
結(jié)果也差不多:
像前文所說,這里線程啟動的方式和剛才的稍有不同,因為新建的的這個類只是實現(xiàn)了 Runnable 接口,所以還需要一個線程來“代理”執(zhí)行它,所以需要把我們新建的這個類的實例傳入到一個線程里,這里其實是代理模式。這個設計模式之后再細講。
小結(jié)
那這兩種方式哪種好呢?
使用 Runnable 接口更好,主要原因是 Java 單繼承。
另外需要注意的是,在啟動線程的的時候用的是 start(),而不是 run()。
調(diào)用 run() 僅僅是調(diào)用了這個方法,是普通的方法調(diào)用;而 start() 才是啟動線程,然后由 JVM 去調(diào)用該線程的 run() 。
好了,以上就是多線程第一篇的所有內(nèi)容了,這里主要是幫助大家復習一下基礎概念,以及沒有接觸過多線程的小伙伴可以入門。想看更多關(guān)于多線程的文章的話,記得給我點贊留言哦~