文章目錄
- JavaWeb 基礎知識(二)多執行緒01
- 一、認識執行緒
- 0.執行緒的引入
- 1.執行緒的概念
- 2.行程與執行緒
- 例子
- 二、Java中的執行緒
- 1.執行緒的創建
- (1)run 和 start
- (2)創建執行緒的幾種方式
- (3)jconsole 查看執行緒資訊
- (4)多執行緒的優勢-增加運行速度
- 2.Thread 的常見構造方法
- 3.Thread 的幾個常見屬性
- ID
- 名稱
- 狀態
- 優先級
- 后臺執行緒
- 存活
- 未完待續...
JavaWeb 基礎知識(二)多執行緒01
上節回顧
??我們在介紹本節內容之前,先來簡單復習一下上一節行程的相關內容

一、認識執行緒
0.執行緒的引入
??引進行程的目的,就是為了能夠"并發編程"
??雖然多行程已經能夠解決并發的問題了,但是我們認為,還不夠理想,
創建行程、銷毀行程、調度行程開銷有點大了
行程時系統資源分配的基本單位
創建行程,就需要分配資源
銷毀行程,就需要釋放資源
??于是程式員就發明了一個 “執行緒”(Thread)的概念,執行緒在有些系統上也被叫做"輕量級行程".
輕量:
創建執行緒比創建行程更高效
創建執行緒比銷毀執行緒更高效
調度執行緒比調度行程更高效

1.執行緒的概念
一個執行緒就是一個 “執行流”. 每個執行緒之間都可以按照順訊執行自己的代碼. 多個執行緒之間 “同時” 執行著多份代碼.
我們站在系統內核的角度,再來看行程和執行緒
一個系統之中可能有很多個PCB(行程控制塊),各個PCB通過鏈表進行連接,如圖代表系統中已有的4個行程 ( pid 分別代表著行程id )
在Linux系統中,執行緒同樣是使用PCB來描述的
行程1,對應一個PCB,在這個行程1里創建一個執行緒,也是再加了一個PCB
??這就是當前我們看到的一個情況,那其實,站在作業系統內核的角度,不分“執行緒”還是“行程”,系統只認PCB
??我們用戶在創建一個行程出來,系統內核方面就會有一個PCB插入到雙向鏈表里面,如果我們在代碼中再去創建一個新的執行緒,也就是再加一個PCB
??像上面的 行程2、行程3、行程4,他們看起來沒有創建其他執行緒,但是行程創建之初,也會有一個PCB產生,我們可以把PCB視作里面的一個執行緒
我們可以得到一個結論:
??當我們創建了一個行程的時候,就是創建了一個PCB 出來,同時這個PCB也可以是當做是目前行程已經包含了一個執行緒了,所以一個行程中至少有一個執行緒,
??同一個行程的執行緒之間,是可以共用一份記憶體空間的,同時其他的行程(PCB)使用的是獨立的記憶體
??也就是說上面的行程中,行程1和執行緒1 共用一份記憶體空間,行程2、行程3、行程4都有自己獨立的記憶體空間,
??這就是我們站在系統內核的角度描述執行緒基本的情況
那我們又茍訓來了,執行緒和代碼有啥關系呢?
可以認為,一個執行緒就是代碼中的一個執行流
執行流:按照一定的順序來執行一組指令
2.行程與執行緒
??行程與執行緒之間本來就是容易搞混淆的,尤其是對于Linux系統來說,行程和執行緒之間又是存在著千絲萬縷的聯系,總之呢,我們得知道 行程和執行緒之間 的區別和聯系
經典面試題
行程和執行緒之間的區別和聯系[面試題]
1、行程是包含執行緒的,一個行程里可以有一個執行緒,同時也可以有多個執行緒,
2、每個行程都有獨立的記憶體空間(虛擬地址空間),同一個行程的多個執行緒之間,共用一個虛擬地址空間,
3、行程是作業系統分配資源的基本單位,執行緒是作業系統調度執行的基本單位
??上節課所介紹的"行程調度",當時咱們是沒有考慮執行緒的,實際上系統是以執行緒(PCB)為單位進行調度執行的.
咱們來畫圖說明:

3個行程4個執行緒
??我們先讓CPU處理 第一個PCB塊,執行一段時間之后,把PCB1 釋放,再來執行PCB2,執行一段時間后,再進行釋放,所以系統是根據PCB進行調度執行的.
??以上就是我們所講的 執行緒和行程之間的區別與聯系,上面的三點大家一定要有印象,是后面在面試的時候經常問到的問題,
例子
??剛才我們都一直在干巴巴的講理論,可能是有點抽象了,那我們就再舉一個例子進行說明一下吧(很形象)
主角:滑稽老鐵
道具:封閉的房間與桌上的100只雞

現在呢,房間里的桌上有100只雞,如何提高滑稽老鐵吃雞的速度?
那么此時,我們就有兩種方案:
1、多行程
那么多行程是怎么吃呢?
多行程吃雞
現在有兩個房間,兩套桌子,把雞平均分成兩份,兩個滑稽老鐵同時再房間各自吃50只雞
??這種分配的方法相比較于之前明顯吃雞的速度要高效好多,
??這就是我們所說的并發編程的效率,能夠提升整體程式的效率
兩個房間、兩套桌子,說明每次再創建行程,都要給這個行程分配一些資源
這兩個房間里的滑稽老鐵,相互之間都看不見彼此,說明行程之間有獨立的地址空間(行程的隔離性)
??這就是我們所說的多行程的吃雞版本!
??當然了,兩個房間、兩套桌子總體來說成本還是有點高的,所以我們為了降低成本,那么我們還可以多執行緒吃雞~
2、多執行緒
多執行緒吃雞怎么吃?我們這樣做~
還是一個房間,只不過多了一個滑稽老鐵來一塊吃雞
多了一個滑稽老鐵(多了一個吃雞的執行流)
??每個滑稽吃50只雞就行了 ~ 1個人吃50只肯定比1個人吃100只速度更快一些
這里還有一個很重要的問題:
??這兩個滑稽老鐵共用了同一個房間和桌子,一個行程的多個執行緒之間,共用一個虛擬地址空間.同時,這兩個滑稽老鐵是可以看到對方的情況的
只創建了一個滑稽,桌子和房間都沒有新創建,創建這個執行緒的成本比創建行程的成本要更低.
??這就是多執行緒吃雞的一個情況,那么接下來呢?
??我們多執行緒吃雞吃著吃著覺得效率還不夠高,還可以進一步怎么提高效率呢?
進一步提高效率:再多搞幾個滑稽(執行緒)
??滑稽的數目(執行緒的數目)更多了,每個滑稽的任務就更少了,因此整體的效率就更高了~~
??就是說隨著我們執行緒數目的增多,執行緒去完成同一個任務,我們的速度就會更快
??但是大家注意,這里的速度也不是說執行緒的數目越多越好!!如果執行緒的數目太多了,執行緒之間就會更加頻繁的進行調度,調度的開銷也就無法忽略了!!
就會出現下面的情況
我們增加了滑稽(執行緒)的數目,就可能出現有的滑稽搶不上位置(CPU),于是任務的執行速度反而會變慢,這什么意思呢?
??沒有搶著位置的三個老鐵,為了吃雞,要往里面擠,于是已經圍著桌子吃起來的滑稽老鐵們就沒有辦法消停的吃雞了,有的滑稽就可能本來吃的好好的,沒一會被擠出來了,這樣就會出現很多問題~
??所以執行緒也不是越多越好,執行緒的數目越多,就會引發更多的調度開銷,反而可能讓執行任務速度變得更慢~所以這一點呢,大家也要明確.
??還有一種情況,當我們很多滑稽老鐵一起吃雞的時候,可能有打架的行為~~
什么叫打架的情況呢?
還是剛才的飯局
兩個滑稽(執行緒)同時看上一個雞大腿(準備修改同一塊記憶體的資料),這個時候就會起沖突.
??這種情況,我們稱為"執行緒不安全",這同樣也是多執行緒編程的重點問題,在后面的章節會著重介紹!!
還有一種情況,如果某個滑稽老鐵不開心,某個滑稽(執行緒)一直搶不到桌子的位置
于是這個老鐵一生氣,把桌子給掀了!!
這說明什么呢?
一個行程里面如果某個執行緒拋出了例外,并且沒有合理catch住的話,就可能導致整個行程都例外退出.其他執行緒也就玩完了
??所以一個執行緒不作業,其他的執行緒也全都不作業了,這一點,就對我們的多執行緒程式的安全性提了更高的呃要求
??多執行緒程式的撰寫,其實就提出了一個更高的要求,一定要保證執行緒的穩定
講到這呢,那么我們滑稽的案例就告一段落了~
希望通過這樣的一個例子,讓大家更好的了解行程與執行緒之間的關聯關系
??以上就是我們所介紹的行程與執行緒的關聯與特點,準確的來說是執行緒的一些特點,這些特點我們在以后寫代碼的時候也就會逐步的感受到了~好了,說了那么多,都是理論的知識,理論的知識大家有一個簡單的認識就可以了,重點我們還是要落在代碼上!!
??那么接下來,我們就介紹 使用Java來操作執行緒Thread類(創建執行緒)的相關方法
二、Java中的執行緒
??在Java當中,是使用Thread這個類的物件來表示一個作業系統中的執行緒
PCB是在作業系統內核中,描述執行緒的
而Thread類則是在Java的代碼中 描述執行緒的.
接下來,我們就來寫一下簡單的代碼來創建執行緒出來~
1.執行緒的創建
??首先我們得去創建Thread 的實體出來,但是常見的方式并不是直接new一個物件出來.
??Thread 是Java標準庫中描述的一個關于執行緒的類.
??常見的方式就是自己定義一個類繼承Thread,然后重寫Thread中的 run 方法,run 方法就表示執行緒要執行的具體任務(代碼)

start 方法,會在作業系統中真的創建一個執行緒出來(同時在內核中會創建PCB,加入到雙向鏈表當中)
執行一下

這個新的執行緒,就會執行 run中所描述的代碼
??看完這個執行緒的創建程序,有的同學不禁會問了,
??我們在 執行代碼的時候 不用 t.start ,直接執行 t.run 行不行?咱們剛才不是把代碼的邏輯定義到run方法里面了嘛,那我們直接呼叫t.run 不是一樣會執行代碼嘛??
執行一下

那么run 和 start 方法有什么區別呢?
(1)run 和 start
重點:經典面試題
run 和 start 的區別是非常非常大的,我們來給大家具體演示一下這個情況.
start 方法
當我們運行Java代碼的時候,首先系統會創建一個行程,這個行程里面已經包含了一個執行緒了,這個執行緒執行的代碼默認就是 main 方法 ,main方法呼叫t.start方法,在系統中又會創建一個執行緒(PCB)出來,然后這個PCB執行任務代碼.

run 方法
run 方法沒有創建新的PCB,沒有創建新的執行緒.

t.run 這里并沒有創建出一個新的執行緒,
而使用t.start 這個方法可以創建出新的執行緒,同時t.start 的兩個執行緒之間是屬于同一行程,屬于并發的關系
例子
如果大家還是沒有懂的話,給大家再舉一個例子:
比如說老王想買一瓶醬油,start 方法就是 老王把兒子小王叫來說,你去樓下超市去買一瓶醬油,run方法就是 老王自己去買一瓶醬油,
這兩種方式的區別,尤其是start,就是在派小王去買醬油的同時,老王自己同時想干嘛就干嘛,這就是并發的效果.而run方法只能老王只能去買醬油了,沒法干其他別的事
這樣的一個區別大家一定要區分請
讓大家看一下程式并發的效果

??在上面的代碼中,Mythread 中執行的是一個死回圈,他會一直回圈執行,在主執行緒Main里也會一直執行回圈,那么都是死回圈,這兩個代碼能同時執行嗎?
??按照上面的講解,MyThread 是一個執行流,Main 也是一個執行流,他們屬于并發的關系,所以可以同時執行!
那么運行程式,觀察結果~

??“hello main” 和 “hello thread” 進行交替列印,進一步驗證了兩個執行緒并發執行的效果,
??通過剛才這個代碼,我們就可以看到,我們通過執行緒可以讓兩個死回圈按照并發執行的方式,一起來執行,而不是單純的說,一個執行完才去執行另外一個,好了,這是我們通過 start 創建執行緒來這樣做的,如果我們改成 run呢?

執行代碼,觀察結果

??就會在MyThread 的死回圈中轉不出去,main的回圈無法執行
??這里我們也可以看到 start 和 run之間很本質的區別,run 并沒有創建出新的執行緒,它屬于一個執行緒里面串行執行,而通過start 就可以創建新的執行緒,可以是兩個執行緒以并發的方式同時來執行.
以上就是關于執行緒最基本的代碼~
(2)創建執行緒的幾種方式
??在上面的程式中,我們是通過新建一個類繼承標準庫中的Thread類來創建執行緒的,實際上,執行緒的創建是有很多種方式的,下面我們就來了解一下 Java當中創建執行緒的幾種方式.
1.繼承Thread,重寫run
??我們自己建一個類繼承Thread ,在這個類中重寫run方法.
**加粗樣式**
class Mythread1 extends Thread{
@Override
public void run() {
// 執行的任務
}
}
2.實作Runable介面,重寫 run
Runable 是標準庫提供的一個介面,這個介面主要用于描述"一個任務",里面也是有一個核心的run方法,通過run方法來描述具體要執行的任務代碼是什么…
class MyRunable implements Runnable{
@Override
public void run() {
//執行的任務
while(true){
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Thread1 {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunable());
t1.start();
}
}
注意: ??我們的Runable 并不是能夠獨立去使用,還要搭配我們的 Thread 類來進行使用,new MyRunable 的實體作為 Thread 的引數,這就是當前這種方式的寫法.
本質上和剛才繼承Thread重寫Run的效果一樣,都是具體告訴執行緒具體要執行的任務是什么…
只不過MyRunable只是用來描述一個具體的任務是什么,而真正執行緒的主體還是在于我們的Thread類本身.
??同時這種方式,還可以給當前創建的執行緒賦予名字,名字作為Thread 的第二個引數

3.繼承Thread,重寫run(匿名內部類)
內部類:在一個類里面定義類
所以匿名內部類就是一個沒有類名的內部類,沒有類名也沒有關系,至少可以創建出一個實體來~
那么我們什么時候需要用到匿名內部類呢?
只需要這個實體,不需要用到其他實體了,
匿名內部類的方式寫起來更加簡潔一些
那么下面我們具體來看具體的代碼應該怎么寫…
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
// 執行任務代碼
}
};
}
??創建了一個匿名的類,這個類繼承了 Thread,此處new 的實體其實是Thread類的子類的實體
4.實作Runnable 重寫run,使用匿名內部類
??因為都是用的匿名內部類,所以這兩種寫法都很像,但是我們仔細觀察會發現,寫法不太相同.


我們把兩組代碼拿過來對比一下,可以看出區別來
第一種:我們創建匿名內部類是Thread 的子類,所以 {} 跟在 Thread 的后面
第二種:我們創建的是一個Runnable 這樣的子類,new的 Runnable 子類作為 Thread 的一個引數,相當于創建了一個匿名內部類的實體,把這個實體作為 Thread的引數,
這兩種寫法還是有一定區別的.
??以上的這些創建執行緒的方式,本質上都相同
??只不過區別是,指定執行緒要執行的任務的方式不一樣,此處的區別,其實都是單純的Java語法層面的區別~ 所以這樣的區別并不是很關鍵,這樣的寫法大家只需要多寫兩次去熟練就會了…
好了,寫到這里,可能有同學說了
我們已經了解了創建執行緒的幾種方式了,也知道如何并發執行了,那么有沒有像任務管理器一樣的東西讓我們能夠看到 Java創建的執行緒呢?
(3)jconsole 查看執行緒資訊
??在 JDK 中內置了一個 jconsole 工具,就可以看到執行緒的資訊.
我們先在Java運行一個執行緒

點擊運行,看一看jconsole 里面的執行緒資訊
jconsole 在哪里找呢?
先找到我們的jdk檔案,bin目錄下就有 jconsole.exe
打開jconsole 之后出現這樣的界面

選擇本地行程Main,然后點擊連接
注意在這里,顯示的行程只是Java相關的行程,非Java的行程顯示不出來

??這些執行緒都是當前行程的執行緒,對于一個Java程式來說,啟動的時候不僅啟動了main這樣一個執行緒(main這個執行緒是 main方法對應的執行緒, thread-0 這個就是我們自己創建的新的執行緒),還有很多其他的執行緒,這些執行緒都是JVM在運行的時候內置的一些執行緒…
我們可以通過 這個工具查看每個執行緒的具體情況
如果寫的程式,發現程式掛了,就可以通過 jconsole 來查看程式里面每個執行緒的情況,對于分析解決問題就有很大幫助了
??以上就是用 jconsole 來查看 執行緒相關資訊的具體操作,當然了我們還可以根據其他的資訊來查看,我們就暫時不去介紹這么多了~
??好了,我們繼續執行緒的另一塊知識~
(4)多執行緒的優勢-增加運行速度
??之前我們介紹并發編程能夠提高程式的效率,我們呢就通過 Java 的代碼來了解一下 并發編程的效率
這個代碼我們要干什么呢?
首先我們有一個很大的數字,這個數字是10億
首先是串行執行代碼,a、b分別自增10億次

我們來看一下執行結果:

然后是并發執行,讓a、b分別在兩個執行緒中并發執行自增操作,然后計時.

運行查看結果:

當前呢,使用并發的方式 確實比 串行的方式時間上 效率提高很多,
串行執行 600—700 ms
并發執行 300—400 ms
速度確實提高了好多
速度提高正好是提高一倍嘛?
不是~(不一定)
主要是因為執行緒調度自身也是有開銷的~
串行執行: 一個執行緒執行了20億次回圈,中間可能調度若干次
并發執行:兩個執行緒各自執行10億次回圈,中間可能調度若干次.
因為系統有調度,所以會對程式的運行時間有影響
2.Thread 的常見構造方法
| 方法 | 說明 |
|---|---|
| Thread() | 創建執行緒物件 |
| Thread(Runnable target) | 使用 Runnable 物件創建執行緒物件 |
| Thread(String name) | 創建執行緒物件,并命名 |
| Thread(Runnable target, String name) | 使用 Runnable 物件創建執行緒物件,并命名 |
| 【了解】Thread(ThreadGroup group,Runnable target) | 執行緒可以被用來分組管理,分好的組即為執行緒組,這個目前我們了解即可 |
具體代碼使用:
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("這是我的名字");
Thread t4 = new Thread(new MyRunnable(), "這是我的名字");
3.Thread 的幾個常見屬性
| 屬性 | 獲取方法 |
|---|---|
| ID | getId() |
| 名稱 | getName() |
| 狀態 | getState() |
| 優先級 | getPriority() |
| 是否是后臺執行緒 | isDaemon() |
| 是否存活 | isAlive |
| 是否被中斷 | isInterrupted |
ID
ID 是執行緒的唯一標識,不同執行緒不會重復
名稱
名稱是各種除錯工具會用到
狀態
狀態表示執行緒當前所處的一個情況,和上一節說的"行程的狀態"是類似的效果,存在的意義都是輔助進行執行緒調度
優先級
優先級高的執行緒理論上來說更容易被調度到,和上節課"行程的優先級"是類似的效果
??此處的狀態和優先級 ,和PCB中的狀態優先級并不完全一致,Java執行緒在這個基礎上有做了自己的豐富.
后臺執行緒
關于后臺執行緒,需要記住一點:JVM會在一個行程的所有非后臺執行緒結束后,才會結束運行,
我們在Java中創建的執行緒一般默認都是非后臺執行緒,此時,如果main方法結束了,執行緒還沒結束,JVM不會結束
如果當前執行緒是后臺執行緒,此時如果main方法結束了,執行緒還沒結束,那么JVM行程會直接結束,同時也把這個后臺執行緒給帶走了.
存活
是否存活,即簡單的理解,為 run 方法是否運行結束了
存活是什么意思呢》我們來畫一下

t中的代碼執行完之后,Java中的執行緒PCB也會同時銷毀嗎?并不會.
Java 中PCB物件在JVM 垃圾回識訓制下才會被銷毀,而作業系統內核的 PCB 在代碼執行完之后就銷毀了
所以我們就可以通過 isAlive() 判斷內核中的PCB是否存在
我們對當前程式中的執行緒查看屬性
class MyRunable implements Runnable{
@Override
public void run() {
//執行的任務
while (true){
System.out.println("hello wolrd");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Thread1 {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunable(),"執行緒01");
t1.run();
System.out.println("id: "+t1.getId());
System.out.println("name: "+t1.getName());
System.out.println("Priority: " +t1.getPriority());
System.out.println("State: "+t1.getState());
System.out.println("isAlive: "+t1.isAlive());
System.out.println("isDeamon: "+t1.isDaemon());
}
}


??好了,今天的執行緒就講到這里,希望大家多多復習~
謝謝欣賞!!
下一篇 JavaWeb基礎知識(三)——執行緒02 敬請期待~
未完待續…
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/319704.html
標籤:java











