在寫代碼的時候,可以使用多進程進行并發編程(在Java中,不太推薦,很多很多關于進程相關的API,在Java標準庫中,都沒有提供),也可以使用多線程進行并發編程(系統提供了多線程編程的API,Java標準庫,已經將這些API封裝了,在代碼中可以直接使用)。
目錄
創建線程
方法1 繼承Thread類
方法2: 實現Runnable接口,重寫run方法
方法3. 繼承Thread,重寫run,但是使用匿名內部類
方法4. 實現Runnable,重寫 run,匿名內部類
方法5. 使用 lambda 表達式(常用)
完!
創建線程
方法1 繼承Thread類
繼承Thread來創建一個線程類
細節補充:
? ? ? ? 1. 第一行代碼: class MyThread extends Thread 中,繼承的Thread這個類,好像可以直接使用,并不像Scanner,ArrayList,Random一樣需要導包,這是為什么呢?
? ? ? ? 答:Java標準庫中,有一個特殊的包,java.lang包,這個包中包含了Thread類,且這個lang包是Java自動引入的。
? ? ? ? 2.自己創建的類MyThread中,類前面沒有public,這是怎么回事?前面能不能寫public?
? ? ? ? 答:一個 .java文件中,只能有一個public的類。 一個類前面如果沒有范圍限定符,那就默認是包級作用域,就是只能在當前包中被其他類使用。
? ? ? ? 3.怎么理解run方法是線程的入口方法?
? ? ? ? 答:此處的run方法,并不需要程序員來手動的進行調用,該方法會在線程創建好之后,一個合適的時機,被JVM自動的調用執行。就類似于,main方法,是一個java進程的入口方法。一個進程中,至少會有一個線程,這個進程中的第一個線程,也就稱為”主線程“。main方法,也就是主線程的入口方法。
補充:這種風格的函數,稱之為”回調函數“(callback)
在Java數據結構中,優先級隊列PriorityQueue,在使用的時候,必須要給對象實現一個比較的接口,才能進行使用。
回調函數 :作為參數傳遞給另一個函數,在該函數內部的某個特定時刻被調用執行的函數。其核心特點是函數的調用時機由其他代碼控制。run方法的函數的調用時機由其他代碼(這里是 JVM)控制。
? ? ? ? 4.run方法上面的 @Override 是什么意思?
? ? ? ? 答:方法重寫。本質上:是讓程序員能夠對現有的類,進行擴展。我們要搞一個線程,肯定是要讓這個線程執行一些代碼的,Thread類本身會帶有一個run方法。但很明顯,標準庫自帶的run方法,是不知道我們的具體需求的,業務要求是什么樣?必須要手動的進行指定,Override就是針對原有的Thread進行擴展(把一些能夠復用的,進行了重用,把需要擴展的,進行擴展)。Thread這個API中,會有很多的屬性方法,大部分內容直接復用即可,把需要擴展的內容,進行擴展即可。
? ? ? ? 那如果沒有@Override這個注解,貌似也可以實現方法重寫呀。為啥還要寫這個注解呢?語法中有很多的機制,是方便讓編譯器,對我們的代碼進行自動檢查的。(人是非常不靠譜的!!!機器是較為靠譜的!!!)(就比如fina限定符,限定一個變量不能再被修改,就是方便讓編譯器為我們進行自動檢查的)
? ? ? ? 5.一般來說,實例化的時候都是方式2。
? ? ? ? ?6.什么是操作系統的”內核“?
? ? ? ? 答:操作系統中,最核心部分的功能模塊(管理硬件,給軟件提供穩定的運行環境)
? ? ? ? 操作系統大致可以分為內核空間(內核態)和用戶空間(用戶態),平時運行的普通的應用程序,網易云音樂,idea,qq等等,都是運行在用戶態的。但是,這些應用程序,有時候,需要針對一些系統提供的硬件資源來進行操作。這些操作,都不是應用程序直接操作的,就是需要調用操作系統提供的API,進一步在內核中完成這樣的操作。
? ? ? ? 為啥還要分出用戶態和內核態?
? ? ? ? 最主要的目的還是穩定。防止某些應用程序把硬件設備或者軟件資源給搞壞了。系統封裝了一些API,這些API都屬于是一些”合法“的操作,不會對硬件設備或者軟件資源有破壞,應用程序只能調用這些API來實現對應的功能,從而不至于對系統 / 硬件設備產生太大的危害。假如讓應用系統直接操作硬件,在極端條件下,代碼出現bug,會把硬件干壞。
? ? ? ? (就比如是銀行系統,辦事窗口里的工作人員,就是正經的經過培訓政審的安全人員,不會對銀行產生危害,用戶要辦理各種業務,就需要在辦事窗口前,給工作人員說清楚需求,由工作人員代辦)
具體解釋:每個線程都是一個獨立的執行流,每個線程都能夠獨立的去CPU上調度執行。
如下圖代碼:
下面的死循環,是在 t 線程中執行的
而這個死循環,是在 main 線程中執行的
以之前的理解,如果一個代碼中,出現了兩個死循環,則肯定最多只能執行一個,另一個循環就進不去了。但我們把進程運行起來,可以看到,兩個循環,都在執行!這兩個線程,就是兩個獨立的執行流,也就解釋了我們最開始那句話:每個線程都是一個獨立的執行流,每個線程都能夠獨立的去CPU上調度執行。
在調用start方法,創建線程之后,兵分兩路,一路,沿著main方法,繼續執行,打印hello main,另一路,進入到線程的run方法,打印hello thread。是相互獨立的,互不干擾的。
注意:
? ? ? ? 當有多個線程的時候,這些線程的執行的先后順序,是不確定的!!!(這一點,是因為操作系統內核中,有一個”調度器“模塊,這個模塊的實現方法,是一種類似于”隨機調度“的效果)
????????那隨即調度又是什么呢?
1. 一個線程,什么時候調度到CPU上去執行,時機是不確定的。
2. 一個線程,什么時候從 CPU上下來,給別人讓位,時機也是不確定的)
也稱為”搶占式執行“。
所以,每秒鐘到底是先執行 main 還是先執行thread,這是不一定的(隨機調度,搶占式執行)
主線程,在調用 strat 方法之后,就立即往下執行打印了,與此同時,內核就要通過剛才 API 構建出線程,并且執行run。由于創建線程本身也有開銷(雖然開銷比創建進程低,但也不是0).所以,在第一輪打印中,因為創建線程有一定的開銷影響,導致hello thread一般情況下 都比hello main略慢一籌。
我們剛剛,只是通過打印的方式,看到了兩個執行流,還可以通過一些第三方工具,更直觀的來查看多個線程的情況。 --> JDK中,有一個jconsole工具,該工具在JDK的bin文件夾中(該工具,只能列出java的進程,其他不是java的進程,無法進行分析)
注意,我們需要先把進程運行起來,然后再打開工具,找到對應的類名,然后進行鏈接
進入之后,選擇線程
main 對應的就是main方法的主線程
黃色框的,就是我們縮寫的代碼,創建的 t 線程(這個Thread - 0,1,2...是默認名稱 可以改)
剩下的線程,都是JVM自帶的線程,這些自帶的線程,要完成一些垃圾回收,監控統計各種指標....
點進具體的線程,就可以看到相關的調用棧(線程里當前執行到了那個方法的第幾行代碼,這個方法是如何如何一層調用過去的...)
這里這個線程的在不斷的運行的,點擊線程詳細情況的這個瞬間,就相當于咔嚓來一個閃照一樣,把這一瞬間的狀態展示到這里了。
這就是使用?jconsole 來檢測線程的方法
再回頭看我們的舉例代碼
我們在循環中加入了sleep方法,來降低這兩個死循環的速度(在C語言中,用的是Windows api中提供的Sleep函數,Windows.h),我們此處使用的sleep,是Java中封裝后的版本,屬于是Thread提供的靜態方法。
如果我們不進行 try - catch的話,該方法會編譯報錯,為什么必須需要拋出異常呢? --> 這個異常,意味著,在sleep(1000)的過程中,可能會被提前喚醒。(換句話說:sleep(1000)的作用是休眠1s,但在這休眠1s中,可能會被其他操作給提前喚醒,也就是沒休眠夠一秒,就被喚醒了,此處我們就應該在catch塊中 提出具體的解決方法,到底應該怎么做)
還有一點需要注意:
為什么在run方法中,解決sleep的異常只能是try - catch方法
但是在main方法中,可以是try - catch方法,也可以是在方法體上throws出異常
為什么呢?
原因很簡單,run方法中的sleep,如果加上throws的話,就修改了方法簽名了,這樣的話是無法構成”重寫“的,因為父類的run方法中,并沒有throws這個異常,子類重寫的時候,也就不能throws異常。
方法2: 實現Runnable接口,重寫run方法
1.實現Runnable接口
Runnable可以理解成”可執行的“,通過這個接口,就可以抽象表示出一段可以被其他實例來執行的代碼。
?2.創建Thread實例,調用Thread的構造方法時,將Runnable對象作為參數傳入。
Runnable接口,還需要搭配Thread類,才能真正在系統中創建出線程
3.調用start方法
上面這種寫法,其實是把 線程 和 要執行的任務 進行了解耦合了。
方法3. 繼承Thread,重寫run,但是使用匿名內部類
匿名內部類:是一種內部類,在一個類里面定義的類,其最大的特點是-->沒有名字,不能重復使用,用一次之后就找不到了。
解釋:
寫這個 { } 的意思是:定義一個類,與此同時,這個新的類,繼承自Thread。此處的 { } 中,可以定義子類的屬性和方法。此處最主要的目的就是重寫 run 方法。與此同時,這個代碼,還創建了子類的實例。且?t 指向的實例,并非是單純的Thread,而是 Thread的子類(但并不知道這個子類叫什么,因為是匿名的內部類)
方法4. 實現Runnable,重寫 run,匿名內部類
這種方法,在Thread構造方法的參數中,填寫了 Runnable 的匿名內部類的實例
方法5. 使用 lambda 表達式(常用)
在lambda表達式中, () 中是形參列表,這里可以帶參數,但因為線程的入口不需要參數,所以這里為空,() 的前面,應該還有一個函數名,此處作為匿名函數,就沒有名字了。
解釋:
在Java中,方法是不能脫離類單獨存在的,所以就不得不設置回調函數,從而多套了一層。但與此同時,java 語法也開了一個特殊的口子,就是 lambda 表達式,函數式接口,屬于是 lambda 背后的實現,相當于 java 在沒破壞原有的規則的基礎上,給了一個 lambda 一個合法性的解釋。
上述五種方法,都是等價的,可以互相轉換的,只不過是第五種 lambda 表達式的方法,更為簡潔一些,更加常用一些!