現代的計算機,大多遵守 馮諾依曼體系結構 (Von Neumann Architecture)
AMD Ryzen 7 580OU with Radeon Graphics
GHz 叫做 CPU 的主頻
這個數字越大,CPU 就算的越快,表示 1s 執行 32 億條指令
存儲器: 分為外存和内存, 用于存儲數據(使用二進制方式存儲)
輸入設備: 用戶給計算機發号施令的設備.
輸出設備:計算機個用戶彙報結果的設備
針對存儲空間
針對數據訪問速度
認識計算機的祖師爺 – 馮諾依曼
馮·諾依曼(John von Neumann,1903年12月28日-1957年2月8日), 美籍匈牙利數學家、計算機科學家、物理學家,是20世紀最重要的數學家之一。馮·諾依曼是布達佩斯大學數學博士,在現代計算機、博弈論、核武器和生化武器等領域内的科學全才之一,被後人稱為 “現代計算機之父”, “博弈論之父”.
電子開關 —— 機械繼電器 (Mechanical Relay):
電磁繼電器:通過通電 / 不通電來切換開關狀态,得到 1 或者 0 這樣的數據
基于上述的 “電子開關” 就能構造出基本的門電路,可以實現 1 位(bit) 的基本邏輯運算
最基礎的門電路,有三種:
非門:可以對一個 0/1 進行取反. 0-> 1
與門:可以針對兩個 0/1 進行與運算. 1 0 -> 0
或門:可以針對兩個 0/1 進行或運算. 1 0 -> 1
針對二進制數據來進行的.不是"邏輯與”,此處是按位與
借助上述的基礎門電路,能構造出一個更複雜的門電路:異或門
相同為0,相異為1。1 0 -> 1
基于上述的門電路,還可以搭建出一些運算器
半加器:是針對兩個比特位,進行加法運算
基于上述的半加器和全加器,就可以構造出一個針對多個 bit 位的數據進行加法運算的加法器了
電子開關=>基礎的門電路=>異或門電路=>半加器=>全加器=>8位加法器
有了加法器之後,就可以計算不隻是加法,還能計算減法、乘法、除法都是通過這個加法器來進行
CPU裡面除了運算器之外,還有控制單元和寄存器(Register)
門電路 (電子開關)
CPU芯片來說,上面就集成了非常非常多的這些電子開關,一個CPU上面的電子開關越多,就認為是計算能力就越強
CPU裡面除了運算器之外,還有控制單元和寄存器
寄存器是CPU内部用來存儲數據的組件
訪問速度:寄存器是内存的3-4個數量級
存儲空間:比内存小很多很多,現在的x64的cpu (64位的cpu),大概有幾十個寄存器,每個寄存器是8個字節,幾百個字節,
成本:CPU上面的這個寄存器,還是非常貴
持久化:掉電之後數據丢失
控制單元 CU(Control Unit):
協調CPU來去進行工作
控制單元最主要的工作,能夠去執行指令
後面進行詳細的論述
指令和編程密切相關。
編程語言,大概分成三類:
1、機器語言
通過二進制的數字,來表示的不同的操作
不同的CPU (哪怕是同一個廠商,但是不同型号的CPU),所支持的機器語言(指令)也可能存在差别
2、彙編語言
一個CPU到底支持哪些指令,生産廠商,會提供一個**"芯片手冊”** 詳細介紹CPU都支持哪些指令,每個指令都是幹啥的
彙編語言和機器語言是一對一的關系 (完全等價)
不同的CPU支持的機器指令不一樣,不同的CPU上面跑的彙編也不一樣
學校的大部分的彙編語言都是針對一款上古神 U,Intel 8086 CPU
3、高級語言
(C,Java,JS)
指令是如何執行的?
自己構造出一個最簡單的芯片手冊:
假設CPU上有兩個寄存器
A 00
B 01
0010 1010
這個操作的意思,就是把1010 内存地址上的數據給讀取到A寄存器中
0001 1111
這個操作的意思,就是把 1111内存地址上的數據讀到寄存器 B 中
0100 1000
這個操作的意思,就是把 A寄存器的值,存到 1000這個内存地址中
1000 0100
這個操作的意思,就是把 00寄存器和01寄存器的數值進行相加,結果放到 00 寄存器裡
CPU的工作流程:(通過CU控制單元來實現的)
咱們編寫的程序,最終都會被編譯器給翻譯成 CPU 所能識别的機器語言指令,在運行程序的時候,操作系統把這樣的可執行程序加載到内存中,cpu 就一條一條指令的去進行讀取,解析,和執行,如果再搭配上條件跳轉,此時,就能實現條件語句和循環語句
操作系統是一組做計算機資源管理的軟件的統稱。目前常見的操作系統有:Windows系列、Unix系列、
Linux系列、OSX系列、Android系列、iOS系列、鴻蒙等
操作系統是一個搞 "管理的軟件"
exe 可執行文件,都是靜靜的躺在硬盤上的,在你雙擊之前,這些文件不會對你的系統有任何影響
但是,一旦你雙擊執行這些 exe 文件,操作系統就會把這個 exe 給加載到内存中,并且讓 CPU 開始執行exe内部的一些指令 (exe裡面就存了很多這個程序對應的二進制指令)
這個時候,就已經把 exe給執行起來,開始進行了一些具體的工作
這些運行起來的可執行文件,稱為 "進程"
這些都是機器上運行的進程:
描述進程:操作系統裡面主要都是通過 C/C++來實現的,此處的描述其實就是用的C語言中的 “結構體” (也就和Java的類差不多)
**操作系統中描述進程的這個結構體, "PCB" (process control block),進程控制塊,這個東西不是硬件中的那個PCB闆
組織進程:典型的實現,就是使用雙向鍊表來把每個進程的PCB給串起來
操作系統的種類是很多的,内部的實現也是各有不同,咱們此處所讨論的情況,是以Linux這個系統為例,由于windows, mac 這樣的系統,不是開源的,裡面的情況我們并不知道
1、pid (進程id)
進程的身份标識,進程的身份證号
3、文件描述符表:
程序運行過程中,經常要和文件打交道 (文件是在硬盤上的)
文件操作:打開文件,讀/寫文件,關閉文件
進程每次打開一個文件,就會在文件描述符表上多增加一項,(個文件描述符表就可以視為是一個數組,裡面的每個元素,又是一個結構體,就對應一個文件的相關信息)
一個進程隻要一啟動,不管你代碼中是否寫了打開 / 操作文件的代碼,都會默認的打開三個文件 (系統自動打開的),标準輸入(System.in),準輸出(System.out) 标準錯誤(System.err)
要想能讓一個進程正常工作,就需要給這個進程分配一些系統資源:内存,硬盤,CPU
這個文件描述符表的下标,就稱為文件描述符
4、進程調度:
5、并行和并發:
并行和并發這兩件事, 隻是在微觀上有區分
宏觀上咱們區分不了,微觀上這裡的區分都是操作系統自行調度的結果
例如6個核心,同時跑20個任務
這20個任務, 有些是并行的關系, 有些是并發的關系。可能任務A和任務B,一會是并行, 一會是并發….都是微觀上操作系統在控制的,在宏觀上感知不到
正因為在宏觀上區分不了并行并發, 我們在寫代碼的時候也就不去具體區分這兩個詞實際上通常使用 “并發” 這個詞, 來代指并行+并發
咱們隻是在研究操作系統進程調度這個話題上的時候, 稍作區分但是其他場景上基本都是使用并發作為一個統稱來代替的,并發編程
6、調度
所謂的調度就是 “時間管理”,
并發就是規劃時間表的過程,也就是“調度"的過程
7、狀态
狀态就描述了當前這個進程接下來應該怎麼調度
Linux中的進程狀态還有很多其他的…
8、優先級
先給誰分配時間,後給誰分配時間,以及給誰分的多,給誰分的少…
9、記賬信息
統計了每個進程,都分别被執行了多久,分别都執行了哪些指令,分别都排隊等了多久了…
給進程調度提供指導依據的
10、上下文
就表示了上次進程被調度出 CPU 的時候,當時程序的執行狀态。下次進程上CPU的時候,就可以恢複之前的狀态,然後繼續往下執行
進程被調度出CPU之前,要先把CPU中的所有的寄存器中的數據都給保存到内存中 (PCB的上下文字段中) ,相當于存檔了
下次進程再被調度上CPU的時候,就可以從剛才的内存中恢複這些數據到寄存器中,相當于讀檔了
存檔+讀檔,存檔存儲的遊戲信息,就稱為 “上下文”
進程的調度,其實就是操作系統在考慮CPU資源如何給各個進程分配
那内存資源又是如何分配的呢?
11、虛拟地址空間:
由于操作系統上,同時運行着很多個進程,如果某個進程,出現了bug 進程崩潰了,是否會影響到其他進程呢?
現代的操作系統 (windows, linux, mac… ) ,能夠做到這一點,就是 “進程的獨立性” 來保證的,就依仗了"虛拟地址空間"
例:如果某個居民核酸變成陽性了,是否會影響到其他的居民呢?
一旦發現有人陽性了,就需要立刻封樓封小區,否則就會導緻其他人也被傳染,
這個情況就類似于早期的操作系統,早期的操作系統,裡面的進程都是訪問同一個内存的地址空間。如果某個進程出現 bug,把某個内存的數據給寫錯了,就可能引起其他進程的崩潰
解決方案,就是把這個院子,給劃分出很多的道路
這些道路之間彼此隔離開,每個人走各自的道理,這個時候就沒事了,此時即使有人确診,也影響不到别人了,
如果把進程按照虛拟地址空間的方式給劃分出了很多份,這個時候不是每一份就隻剩一點了嘛?? 雖然你的系統有百八十個進程,但是實際上從微觀上看,同時執行的進程,就6個!!
每個進程能夠撈着的内存還是挺多的,而且另一方面,也不是所有的進程都用那麼多的内存,有的進程 (一個3A遊戲,吃幾個G),大多數的進程也就隻占幾M即可
12、進程間通信
進程之間現在通過虛拟地址空間,已經各自隔離開了,但是在實際工作中,進程之間有的時候還是需要相互交互的。
例:某業主A問:兄弟們,誰家有土豆,借我兩個
業主B回答:我有土豆,我給你
設定一個公共空間,這個空間是任何居民都可以來訪問的,
讓B先把土豆放到公共空間中,進行消毒,再讓A來把這個公共空間的土豆給取走,彼此就不容易發生傳染
類似的,咱們的兩個進程之間,也是隔離開的,也是不能直接交互的,操作系統也是提供了類似的 "公共空間”,
進程 A 就可以把數據見放到公共空間上,進程B再取走
進程間通信:
操作系統中,提供的 “公共空間” 有很多種,并且各有特點,有的存儲空間大,有的小,有的速度快,有的慢.….
操作系統中提供了多種這樣的進程間通信機制,(有些機制是屬于曆史遺留的,已經不适合于現代的程序開發)
現在最主要使用的進程間通信方式兩種:
1.文件操作
2.網絡操作 (socket)
總結:
為啥要有進程?因為我們的系統支持多任務了,程序猿也就需要 “并發編程”
通過多進程,是完全可以實現并發編程的,但是有點小問題:
如果需要頻繁的創建而 / 銷毀進程,這個事情成本是比較高的,如果需要頻繁的調度進程,這個事情成本也是比較高的:
對于資源的申請和放,本身就是一個比較低效的操作,
創建進程就得分配資源:
1)内存
2)文件
銷毀進程也得釋放資源
1)内存
2)文件
如何解決這個問題?思路有兩個:
可以把進程比作一個工廠,假設這個工廠有一些生産任務,例如要生産 1w 部手機
要想提高生産效率:
1). 搞兩個工廠,一個生産 5k (多創建了一個進程)
2). 還是一個工廠,在一個工廠裡多加一個生産線,兩個生産線并行生産,一個生産線生産5k,(多創建了一個線程)
最終生産1w個手機,花的時間差不多,但是這裡的成本就不一樣了
多加一些線程,是不是效率就會進一步提高呢?一般來說是會,但是也不一定
如果線程多了,這些線程可能要競争同一個資源,這個時候,整體的速度就收到了限制,整體硬件資源是有限的
總結進程與線程的區别:
Java這個生态中更常使用的并發編程方式,是多線程
其他的語言,主打的并發變成又不一樣:
go,主要是通過多協程的方式實現并發.
erlang,這個是通過 actor 模型實現并發.
JS,是通過定時器+事件回調的方式實現并發.……
多線程仍然是最主流最常見的一種并發編程的方式
Java 的線程 和 操作系統線程 的關系:
Thread類的基本用法
通過 Thread 類創建線程,寫法有很多種
其中最簡單的做法,創建子類,繼承自Thread,并且重寫 run 方法
package thread; class MyThread extends Thread { @Override public void run() { System.out.println("hello thread!");; } } public class demo1 { public static void main(String[] args) { Thread t = new MyThread(); t.start(); } }
run 方法描述了,這個線程内部要執行哪些代碼,每個線程都是并發執行的 (各自執行各自的代碼),因此就需要告知這個線程,你執行的代碼是什麼,
run 方法中的邏輯,是在新創建出來的線程中,被執行的代碼
并不是我一定義這個類,一寫run方法,線程就創建出來,相當于我把活安排出來了,但是同學們還沒開始幹呢
需要調用這裡的 start 方法,才是真正的在系統中創建了線程,才是真正開始執行上面的 run 操作,在調用 start 之前,系統中是沒有創建出線程的
如果在一個循環中不加任何限制,這個循環轉的速度非常非常快,導緻打印的東西太多了,根本看不過來了,就可以加上一個 sleep 操作,來強制讓這個線程休眠一段時間
這個休眠操作,就是強制地讓線程進入阻塞狀态,單位是 ms,就是1s 之内這個線程不會到 cpu 上執行
public void run() { while (true) { System.out.println("hello thread!"); Thread.sleep(1000); } }
這是多線程編程中最常見的一個異常,線程被強制的中斷了,用 try catch 處理
在一個進程中,至少會有一個線程,
在一個 java進程中,也是至少會有一個調用 main 方法的線程 (這個線程不是你手動搞出來的)
自己創建的 t 線程 和 自動創建的 main 線程,就是并發執行的關系 (宏觀上看起來是同時執行)
此處的并發 = 并行 + 并發
宏觀上是區分不了并行和并發的,都取決于系統内部的調度
package thread; // Thread是在java.lang 裡的,java.lang 裡的類都不需要手動導入,類似的還有String class MyThread2 extends Thread { @Override public void run() { while (true) { System.out.println("hello thread!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class demo2 { public static void main(String[] args) { Thread t = new MyThread2(); t.start(); while (true) { System.out.println("hello main"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
運行打印:
/* hello main hello thread! hello thread! hello main hello main hello thread! hello thread! hello main */
現在兩個線程,都是打印一條,就休眠 1s
當1s 時間到了之後,系統先喚醒誰呢?
看起來這個順序不是完全确定 (随機的)
每一輪,1s 時間到了之後,到底是先喚醒 main 還是 thread,這是不确定的 (随機的)
操作系統來說,内部對于線程之間的調度順序,在宏觀上可以認為是随機的 (搶占式執行)
這個随機性,會給多線程編程帶來很多其他的麻煩
寫法一: 創建子類,繼承自 Thread
寫法二: 創建一個類,實現 Runnable 接口,再創建 Runnable實例傳給Thread 實例
通過 Runnable 來描述任務的内容
進—步的再把描述好的任務交給Thread 實例
package thread; // Runnable 就是在描述一個任務 class MyRunnable implements Runnable { @Override public void run() { System.out.println("hello"); } } public class demo3 { public static void main(String[] args) { Thread t = new Thread(new MyRunnable()); } }
寫法三 / 寫法四: 就是上面兩個寫法的翻版,使用了匿名内部類
創建了一個匿名内部類,繼承自 Thread 類,同時重寫run方法,同時再new出這個匿名内部類的實例
package thread; public class demo4 { public static void main(String[] args) { Thread t = new Thread() { @Override public void run() { System.out.println("hello thread!"); } }; t.start(); } }
new 的是Runnable,針對這個創建的匿名内部類,同時new 出的 Runnable` 實例傳給 Thread 的構造方法
package thread; public class demo5 { public static void main(String[] args) { Thread t = new Thread(new Runnable() { @Override public void run() { System.out.println("hello thread!"); } }); t.start(); } }
通常認為Runnable 這種寫法更好一點,能夠做到讓線程和線程執行的任務,更好的進行解耦
寫代碼一般希望,高内聚,低耦合
Runnable 單純的隻是描述了一個任務,至于這個任務是要通過一個進程來執行,還是線程來執行,還是線程池來執行,還是協程來執行,Runnable 本身并不關心,Runnable 裡面的代碼也不關心
第五種寫法: 相當于是第四種寫法的延伸,使用 lambda 表達式,是使用lambda 代替了 Runnable 而已
package thread; public class demo6 { public static void main(String[] args) { Thread t = new Thread(() -> { System.out.println("hello thread!"); }); t.start(); } }
多線程能夠提高任務完成的效率
測試:有兩個整數變量,分别要對這倆變量自增10億次,分别使用一個線程,和兩個線程
此處不能直接這麼記錄結束時間,别忘了,現 在這個求時間戳的代碼是在 main 線程中
main 和t1 ,t2 之間是并發執行的關系,此處t1和t2 還沒執行完呢,這裡就開始記錄結束時間了,這顯然是不準确的
正确做法應該是讓main線程等待 t1和 t2 跑完了,再來記錄結束時間
join 效果就是等待線程結束,t1.join就是讓main 線程等待t1 結束,t2.join讓 main 線程等待 t2結束
package thread; public class demo7 { private static final long count = 10_0000_0000; public static void serial() { long begin = System.currentTimeMillis(); long a = 0; for (int i = 0; i < count; i++) { a++; } long b = 0; for (int i = 0; i < count; i++) { b++; } long end = System.currentTimeMillis(); System.out.println("消耗時間: " + (end- begin) + "ms"); } public static void concurrency() throws InterruptedException { long begin = System.currentTimeMillis(); Thread t1 = new Thread(() -> { long a = 0; for (int i = 0; i < count; i++) { a++; } }); t1.start(); Thread t2 = new Thread(() -> { long b = 0; for (int i = 0; i < count; i++) { b++; } }); t2.start(); t1.join(); // 讓 main 線程等待 t1 結束 t2.join(); // 讓 main 線程等待 t2 結束 long end = System.currentTimeMillis(); System.out.println("消耗時間: " + (end- begin) + "ms"); } public static void main(String[] args) throws InterruptedException { // serial(); concurrency(); } }
串行執行的時候,時間大概是600多ms (平均650左右)
兩個線程并發執行,時間大概是400多ms (平均450左右)
提升了接近50%
并不是說,一個線程600多ms,兩個線程就是300多ms
這倆線程在底層到底是并行執行,還是并發執行,不确定,真正并行執行的時候,效率才會有顯著提升
多線程特别适合于那種CPU密集型的程序,程序要進行大量的計算,使用多線程就可以更充分的利用CPU的多核資源
方法 | 說明 |
---|---|
Thread() | 創建線程對象 |
Thread(Runnable target) | 使用 Runnable 對象創建線程對象 |
Thread(String name) | 創建線程對象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 對象創建線程對象,并命名 |
【了解】Thread(ThreadGroup group, Runnable target) | 線程可以被用來分組管理,分好的組即為線程組,這 個目前我們了解即可 |
Thread(String name):這個東西是給線程 (thread對象) 起一個名字,起一個啥樣的名字,不影響線程本身的執行
僅僅隻是影響到程序猿調試,可以借助一些工具看到每個線程以及名字,很容易在調試中對線程做出區分
可以使用 jconsole 來觀察線程的名字,jconsole是jdk自帶的一個調試工具
屬性 | 獲取方法 |
---|---|
ID | getId() |
名稱 | getName() |
狀态 | getState() |
優先級 | getPriority() |
是否後台線程 | isDaemon() |
是否存活 | isAlive() |
是否被中斷 | isInterrupted( |
是否後台線程:isDaemon()
如果線程是後台線程,不影響進程退出
如果線程不是後台線程 (前台線程),就會影響到進程退出
創建的 t1 和 t2 默認都是前台的線程
即使 main 方法執行完畢,進程也不能退出,得等 t1 和 t2 都執行完,整個進程才能退出!
如果 t1 和 t2 是後台線程,此時如果 main 執行完畢,整個進程就直接退出,t1 和 t2 就被強行終止了
是否存活: isAlive()
操作系統中對應的線程是否正在運行
Thread t 對象的生命周期和内核中對應的線程,生命周期并不完全一緻
創建出t對象之後,在調用 start 之前,系統中是沒有對應線程的
在run方法執行完了之後,系統中的線程就銷毀了,但是t這個對象可能還存在,通過 isAlive就能判定當前系統的線程的運行情況
如果調用 start之後,run執行完之前,isAlive 就是返回true 。如果調用start 之前,run執行完之後,isAlive 就返回 false
ID是線程的唯一标識,不同線程不會重複
名稱是各種調試工具用到
優先級高的線程理論上來說更容易被調度到
關于後台線程,需要記住一點:JVM會在一個進程的所有非後台線程結束後,才會結束運行。
是否存活,即簡單的理解,為 run 方法是否運行結束了
public class ThreadDemo { public static void main(String[] args) { Thread thread = new Thread(() -> { for (int i = 0; i < 10; i++) { try { System.out.println(Thread.currentThread().getName() + ": 我還活着"); Thread.sleep(1 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ": 我即将死去"); }); System.out.println(Thread.currentThread().getName() + ": ID: " + thread.getId()); System.out.println(Thread.currentThread().getName() + ": 名稱: " + thread.getName()); System.out.println(Thread.currentThread().getName() + ": 狀态: " + thread.getState()); System.out.println(Thread.currentThread().getName() + ": 優先級: " + thread.getPriority()); System.out.println(Thread.currentThread().getName() + ": 後台線程: " + thread.isDaemon()); System.out.println(Thread.currentThread().getName() + ": 活着: " + thread.isAlive()); System.out.println(Thread.currentThread().getName() + ": 被中斷: " + thread.isInterrupted()); thread.start(); while (thread.isAlive()) {} System.out.println(Thread.currentThread().getName() + ": 狀态: " + thread.getState()); } }
start() 決定了系統中是不是真的創建出線程
start 和 run 的區别:
package thread; public class demo1 { public static void main(String[] args) { Thread t = new Thread(() -> { while (true) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); // t.run(); while (true) { System.out.println("hello main"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
用 start() 是并發執行,而 run()循環打印 hello thread
run方法隻是一個普通的方法,你在main線程裡調用run,其實并沒有創建新的線程,這個循環仍然是在 main 線程中執行的
既然是在一個線程中執行,代碼就得從前到後的按順序運行,運行第一個循環,再運行第二個循環
for (int i = 0; i < 5; i++) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }
如果改成循環五次,打印五個 hello thread,讓後打印 hello main
中斷線程:讓一個線程停下來
線程停下來的關鍵,是要讓線程對應的 run方法執行完
還有一個特殊的是 main 這個線程,對于main 來說,得是main方法執行完,線程就完了)
如何中斷線程:
1、标志位
可以手動的設置一個标志位 (自己創建的變量,boolean),來控制線程是否要執行結束
package thread; public class demo2 { private static boolean isQuit = false; public static void main(String[] args) { Thread t = new Thread(() -> { while (!isQuit) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); // 隻要把這個 isQuit 設為true,這個循環就退出了,進一步的 run 就執行完了,再進一步就是線程執行結束了 try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } isQuit = true; System.out.println("終止 t 線程!"); } }
運行輸出:
hello thread
hello thread
hello thread
hello thread
hello thread
終止 t 線程!
在其他線程中控制這個标志位,就能影響到這個線程的結束
此處因為,多個線程共用同一個虛拟地址空間,因此,main 線程修改的 isQuit 和 t 線程判定的 isQuit,是同一個值
2、interrupted()
但是,isQuit 并不嚴謹,更好的做法,使用 Thread 中内置的一個标志位來進行判定
Thread.interrupted() 這是一個靜态的方法
Thread.currentThread().isInterrupted() 這是實例方法,其中 currentThread 能夠獲取到當前線程的實例
package thread; public class demo3 { public static void main(String[] args) { Thread t = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } // 在主線程中,調用 interrupt 方法,中斷這個線程 // 讓 t 線程被中斷 t.interrupt(); } }
運行此代碼,打印五次 hello thread 後,出現異常,然後繼續打印 hello thread
調用 t.interrupt() 這個方法,可能産生兩種情況:
1). 如果 t 線程是處在就緒狀态,就是設置線程的标志位為 true
2). 如果 t 線程處在阻塞狀态 (sleep 休眠了),就會觸發一個 InterruptException
這個代碼絕大部分情況,都是在休眠狀态阻塞
此處的中斷,是希望能夠立即産生效果的
如果線程已經是阻塞狀态下,此時設置标志位就不能起到及時喚醒的效果
調用這個 interrupt 方法,就會讓 sleep 觸發一個異常,從而導緻線程從阻塞狀态被喚醒
當下的代碼,一旦觸發了異常之後,就進入了catch 語句,在catch 中,就單純的隻是打了一個日志
printStackTrace 是打印當前出現異常位置的代碼調用棧,打完日志之後,就直接繼續運行
解決方法:
package thread; public class demo3 { public static void main(String[] args) { Thread t = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); // 當前觸發異常後,立即退出循環 System.out.println("這個是收尾工作"); break; } } }); t.start(); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } // 在主線程中,調用 interrupt 方法,中斷這個線程 // 讓 t 線程被中斷 t.interrupt(); } }
運行結果:
hello thread hello thread hello thread hello thread hello thread java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at thread.demo3.lambda$main$0(demo3.java:9) at java.lang.Thread.run(Thread.java:748) 這個是收尾工作
推薦的做法:
咱們一個代碼中的線程有很多個,随時哪個線程都可能會終止
Thread.interrupted() 這個方法判定的标志位是Thread的static成員,一個程序中隻有一個标志位
Thread.currentThread().isInterrupted()這個方法判定的标志位是 Thread的普通成員,每個示例都有自己的标志位,一般就無腦使用這個方法即可
方法 | 說明 |
---|---|
public void interrupt() | 中斷對象關聯的線程,如果線程正在阻塞,則以異常方式通知, 否則設置标志位 |
public static boolean interrupted() | 判斷當前線程的中斷标志位是否設置,調用後清除标志位 |
public boolean isInterrupted() | 判斷對象關聯的線程的标志位是否設置,調用後不清除标志位 |
多個線程之間,調度順序是不确定的,線程之間的執行是按照調度器來安排的,這個過程可以視為是 “無序,随機”,這樣不太好,有些時候,我們需要能夠控制線程之間的順序
線程等待,就是其中一種控制線程執行順序的手段
此處的線程等待,主要是控制線程結束的先後順序
join():調用 join 的時候,哪個線程調用的 join ,哪個線程就會阻塞等待,要等到對應的線程執行完畢為止 (對應線程的 run 執行完)
package thread; public class demo4 { public static void main(String[] args) { Thread t = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); // 在主線程中,使用等待操作,等 t 線程執行結束 try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }
首先,調用這個方法的線程是 main 線程,針對t這個線程對象調用的,此時就是讓 main 等待t
調用 join 之後,main 線程就會進入阻塞狀态 (暫時無法在cpu上執行)
代碼執行到 join` 這一行,就暫時停下了,不繼續往下執行了
那麼join什麼時候能繼續往下走,恢複成就緒狀态呢?
就是等到 t 線程執行完畢 ( t 的 run方法跑完了)
通過線程等待,就是在**控制讓** t先結束,main 後結束,一定程度上的幹預了這兩個線程的執行順序
這是代碼中控制的先後順序,就像剛才寫的自增 100 億次這個代碼,計時操作就是要在計算線程執行完之後再執行
但是 join 操作默認情況下,是死等,不見不散,這不合理
join 提供了另外一個版本,就是可以設置等待時間,最長等待多久,如果等不到,就不等了
try { t.join(10000); } catch (InterruptedException e) { e.printStackTrace(); }
進入 join 也會産生阻塞,這個阻塞不會一直持續下去,如果 10s 之内,t線程結束了,此時 join直接返回
如果10s之後,t 仍然不結束, 此時join 也就直接返回
日常開發中涉及到的一些 "等待” 相關的操作,一般都不會是死等,而是會有這樣的 "超時時間"
Thread.currentThread() 就能夠獲取到當前線程的引用 (Thread 實例的引用),哪個線程調用的這個currentThread,就獲取到的是哪個線程的實例
package thread; public class demo5 { public static void main(String[] args) { Thread t = new Thread(){ @Override public void run() { System.out.println(Thread.currentThread().getName()); // Thread-0 } }; t.start(); // 在 main 線程中調用的,拿到的就是 main 這個線程的實例 System.out.println(Thread.currentThread().getName()); // main } }
this.getName() :對于這個代碼來,是通過繼承 Thread 的方式來創建線程
此時在 run 方法中,直接通過 this,拿到的就是當前 Thread 的實例
Thread t = new Thread(){ @Override public void run() { System.out.println(this.getName()); } }; t.start();
此處的 this 不是指向 Thread 類型了,而是指向 Runnable,而 Runnable 隻是一個單純的任務,沒有 name 屬性的
Thread t = new Thread(new Runnable() { @Override public void run() { System.out.println(this.getName()); // err } }); t.start();
要想拿到線程的名字,隻能通過 Thread.currentThread()
lambda 表達式效果同 Runnable
Thread t = new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); t.start();
sleep 所謂的休眠到底是在幹啥?
進程:PCB+雙向鍊表,這個說法是針對隻有一個線程的進程是如此的
如果是一個進程有多個線程,此時每個線程都有一個PCB,一個進程對應的就是一組PCB了
PCB 上有一個字段tgroupld,這個id其實就相當于進程的id,同一個進程中的若幹個線程的 tgroupld 是相同的
process control block
進程控制塊 和 線程有啥關系?其實 Linux内核不區分進程和線程
進程線程是程序猿寫應用程序代碼,搞出來的詞,實際上 Linux内核隻認PCB !
在内核裡 Linux 把線程稱為輕量級進程
操作系統調度線程的時候,就隻是從就緒隊列中挑選合适的 PCB 到 CPU 上運行,阻塞隊列裡的 PCB 就隻能幹等着,當睡眠時間到了,系統就會把剛才這個 PCB 從阻塞隊列挪回到就緒隊列,以上情況都是在 Linux 系統
内核中的很多工作都依賴大量的數據結構,但凡是需要管理很多數據的程序,都大量的依賴數據結構
進程有狀态:就緒,阻塞
這裡的狀态就決定了系統按照啥樣的态度來調度這個進程,這裡相當于是針對一個進程中隻有一個線程的情況
更常見的情況下,一個進程中包含了多個線程,所謂的狀态,其實是綁定在線程上
Linux 中,PCB 其實是和線程對應的,一個進程對應着一組 PCB
上面說的 “就緒" 和 “阻塞” 都是針對系統層面上的線程的狀态 (PCB)
在 Java 中Thread 類中,對于線程的狀态,又進—步的細化了
1、 NEW:安排了工作, 還未開始行動
把 Thread 對象創建好了,但是還沒有調用 start
public class demo6 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { }); System.out.println(t.getState()); // NEW t.start(); } }
2、 TERMINATED:工作完成了
操作系統中的線程已經執行完畢,銷毀了但是 Thread 對象還在,獲取到的狀态
public class demo6 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { }); t.start(); Thread.sleep(1000); System.out.println(t.getState()); // TERMINATED } }
以上兩個狀态是 Java 内部搞出來的狀态,就和操作系統中的 PCB 裡的狀态就沒啥關系
3、 RUNNABLE:可工作的,又可以分成正在工作中和即将開始工作
就緒狀态,處于這個狀态的線程,就是在就緒隊列中,随時可以被調度到 CPU 上
如果代碼中沒有進行 sleep,也沒有進行其他的可能導緻阻塞的操作,代碼大概率是處在 Runnable 狀态的
public class demo7 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while (true) { // 這裡什麼都不能有 } }); t.start(); Thread.sleep(1000); System.out.println(t.getState()); // RUNNABLE } }
一直持續不斷的執行這裡的循環,随時系統想調度它上cpu都是随時可以的
4、TIMED_WAITING:這幾個都表示排隊等着其他事情
代碼中調用了sleep,就會進入到 TIMED_WAITIN,意思就是當前的線程在一定時間之内,是阻塞的狀态
public class demo7 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); Thread.sleep(1000); System.out.println(t.getState()); // TIMED_WAITING } }
一定時間到了之後,阻塞狀态解除這種情況就是 TIMED_WAITING,也是屬于阻塞的狀态之一
5、BLOCKED:這幾個都表示排隊等着其他事情
當前線程在等待鎖,導緻了阻塞(阻塞狀态之一) --synchronized
6、WAITING:這幾個都表示排隊等着其他事情
當前線程在等待喚醒,導緻了阻塞(阻塞狀态之一) --wait
為啥要這麼細分?這是非常有好處的:
開發過程中經常會遇到一種情況,程序 "卡死” 了
一些關鍵的線程,阻塞了
在分析卡死原因的時候,第一步就可以先來看看當前程序裡的各種關鍵線程所處的狀态
有話要說...