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