當前位置:首頁 > 科技 > 正文

計算機是如何工作的,Java多線程編程

一、馮諾依曼體系

現代的計算機,大多遵守 馮諾依曼體系結構 (Von Neumann Architecture)


CPU 中央處理器: 進行算術運算和邏輯判斷.

AMD Ryzen 7 580OU with Radeon Graphics
GHz 叫做 CPU 的主頻
這個數字越大,CPU 就算的越快,表示 1s 執行 32 億條指令

存儲器: 分為外存和内存, 用于存儲數據(使用二進制方式存儲)
輸入設備: 用戶給計算機發号施令的設備.
輸出設備:計算機個用戶彙報結果的設備

針對存儲空間

  • 硬盤 > 内存 >> CPU

針對數據訪問速度

  • CPU >> 内存 > 硬盤

認識計算機的祖師爺 – 馮諾依曼

馮·諾依曼(John von Neumann,1903年12月28日-1957年2月8日), 美籍匈牙利數學家、計算機科學家、物理學家,是20世紀最重要的數學家之一。馮·諾依曼是布達佩斯大學數學博士,在現代計算機、博弈論、核武器和生化武器等領域内的科學全才之一,被後人稱為 “現代計算機之父”, “博弈論之父”.


二、CPU 基本工作流程 1、邏輯門 1.1、電磁繼電器

電子開關 —— 機械繼電器 (Mechanical Relay):
電磁繼電器:通過通電 / 不通電來切換開關狀态,得到 1 或者 0 這樣的數據


1.2、門電路 (Gate Circuit)

基于上述的 “電子開關” 就能構造出基本的門電路,可以實現 1 位(bit) 的基本邏輯運算
最基礎的門電路,有三種:
非門:可以對一個 0/1 進行取反. 0-> 1
與門:可以針對兩個 0/1 進行與運算. 1 0 -> 0
或門:可以針對兩個 0/1 進行或運算. 1 0 -> 1
針對二進制數據來進行的.不是"邏輯與”,此處是按位與

借助上述的基礎門電路,能構造出一個更複雜的門電路:異或門
相同為0,相異為1。1 0 -> 1


1.3、運算器

基于上述的門電路,還可以搭建出一些運算器

半加器:是針對兩個比特位,進行加法運算


全加器:是針對三個比特位,進行加法運算


1.4、加法器

基于上述的半加器和全加器,就可以構造出一個針對多個 bit 位的數據進行加法運算的加法器了


A和B是兩個8 bit的數字
A0這個數字的第0位(最右)A1
A2
A3

電子開關=>基礎的門電路=>異或門電路=>半加器=>全加器=>8位加法器

有了加法器之後,就可以計算不隻是加法,還能計算減法、乘法、除法都是通過這個加法器來進行


2、寄存器,控制單元(CU)

CPU裡面除了運算器之外,還有控制單元和寄存器(Register)

門電路 (電子開關)
CPU芯片來說,上面就集成了非常非常多的這些電子開關,一個CPU上面的電子開關越多,就認為是計算能力就越強

CPU裡面除了運算器之外,還有控制單元和寄存器

  • 寄存器是CPU内部用來存儲數據的組件
    訪問速度:寄存器是内存的3-4個數量級
    存儲空間:比内存小很多很多,現在的x64的cpu (64位的cpu),大概有幾十個寄存器,每個寄存器是8個字節,幾百個字節,
    成本:CPU上面的這個寄存器,還是非常貴
    持久化:掉電之後數據丢失

  • 控制單元 CU(Control Unit):
    協調CPU來去進行工作
    控制單元最主要的工作,能夠去執行指令
    後面進行詳細的論述


3、指令(Instruction)

指令和編程密切相關。

編程語言,大概分成三類:

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控制單元來實現的)

  1. 從内存中讀取指令
  2. 解析指令
  3. 執行指令

咱們編寫的程序,最終都會被編譯器給翻譯成 CPU 所能識别的機器語言指令,在運行程序的時候,操作系統把這樣的可執行程序加載到内存中,cpu 就一條一條指令的去進行讀取,解析,和執行,如果再搭配上條件跳轉,此時,就能實現條件語句和循環語句


三、操作系統(Operating System)

操作系統是一組做計算機資源管理的軟件的統稱。目前常見的操作系統有:Windows系列、Unix系列、
Linux系列、OSX系列、Android系列、iOS系列、鴻蒙等

操作系統是一個搞 "管理的軟件"

  1. 對下,要管理好各種硬件設備
  2. 對上,要給各種軟件提供穩定的運行環境
1、進程/任務(Process/Task)

exe 可執行文件,都是靜靜的躺在硬盤上的,在你雙擊之前,這些文件不會對你的系統有任何影響
但是,一旦你雙擊執行這些 exe 文件,操作系統就會把這個 exe 給加載到内存中,并且讓 CPU 開始執行exe内部的一些指令 (exe裡面就存了很多這個程序對應的二進制指令)
這個時候,就已經把 exe給執行起來,開始進行了一些具體的工作
這些運行起來的可執行文件,稱為 "進程"

這些都是機器上運行的進程:

  • 線程:
    線程是進程内部的一個部分, 進程包含線程,如果把進程想象成是一個工廠,那麼線程就是工廠裡的生産線,一個工廠裡面可以有一個生産線或者也可以有多個生産線
    咱們寫的代碼,最終的目的都是要跑起來,最終都是要成為一些進程
    對于 java 代碼來說,最終都是通過 java 進程來跑起來的 (此處的這個 java 進程就是咱們平時常說的jvm)
    進程 (process) 還有另一個名字任務 (task)

2、操作系統是如何管理進程的?
  1. 先描述一個進程 (明确出一個進程上面的一些相關屬性)
  2. 再組織若幹個進程 (使用一些數據結構,把很多描述進程的信息給放到一起,方便進行增删改查)

描述進程:操作系統裡面主要都是通過 C/C++來實現的,此處的描述其實就是用的C語言中的 “結構體” (也就和Java的類差不多)
**操作系統中描述進程的這個結構體, "PCB" (process control block),進程控制塊,這個東西不是硬件中的那個PCB闆

組織進程:典型的實現,就是使用雙向鍊表來把每個進程的PCB給串起來
操作系統的種類是很多的,内部的實現也是各有不同,咱們此處所讨論的情況,是以Linux這個系統為例,由于windows, mac 這樣的系統,不是開源的,裡面的情況我們并不知道


3、PCB中的一些屬性:

1、pid (進程id)
進程的身份标識,進程的身份證号


2、内存指針
指明了這個進程要執行的代碼 / 指令在内存的哪裡,以及這個進程執行中依賴的數據都在哪裡
當運行一個exe,此時操作系統就會把這個 exe 加載到内存中,變成進程
進程要執行的二進制指令 (通過編譯器生成的), 除了指令之外還有一些重要的數據

3、文件描述符表:
程序運行過程中,經常要和文件打交道 (文件是在硬盤上的)
文件操作:打開文件,讀/寫文件,關閉文件
進程每次打開一個文件,就會在文件描述符表上多增加一項,(個文件描述符表就可以視為是一個數組,裡面的每個元素,又是一個結構體,就對應一個文件的相關信息)

一個進程隻要一啟動,不管你代碼中是否寫了打開 / 操作文件的代碼,都會默認的打開三個文件 (系統自動打開的),标準輸入(System.in),準輸出(System.out) 标準錯誤(System.err)
要想能讓一個進程正常工作,就需要給這個進程分配一些系統資源:内存,硬盤,CPU

這個文件描述符表的下标,就稱為文件描述符

4、進程調度:

  • 上面的屬性是一些基礎的屬性,下面的一組屬性,主要是為了能夠實現進程調度
    進程調度:是理解進程管理的重要話題,現在的操作系統,一般都是 “多任務操作系統”(前身就是 “單任務操作系統”,同一時間隻能運行一個進程),一個系統同一時間,執行了很多的任務

5、并行和并發:

  • 并行:微觀上,兩個CPU核心,同時執行兩個任務的代碼
  • 并發:微觀上, 一個CPU核心,先執行一會任務1, 再執行一會任務,再執行一會任務…再執行一會任務
    隻要切換的足夠快, 宏觀上看起來, 就好像這麼多任務在同時執行一樣

并行和并發這兩件事, 隻是在微觀上有區分
宏觀上咱們區分不了,微觀上這裡的區分都是操作系統自行調度的結果

例如6個核心,同時跑20個任務
這20個任務, 有些是并行的關系, 有些是并發的關系。可能任務A和任務B,一會是并行, 一會是并發….都是微觀上操作系統在控制的,在宏觀上感知不到
正因為在宏觀上區分不了并行并發, 我們在寫代碼的時候也就不去具體區分這兩個詞實際上通常使用 “并發” 這個詞, 來代指并行+并發
咱們隻是在研究操作系統進程調度這個話題上的時候, 稍作區分但是其他場景上基本都是使用并發作為一個統稱來代替的,并發編程


4、CPU 分配 —— 進程調度(Process Scheduling)

6、調度
所謂的調度就是 “時間管理”,
并發就是規劃時間表的過程,也就是“調度"的過程

7、狀态
狀态就描述了當前這個進程接下來應該怎麼調度

  • 就緒狀态:随時可以去 CPU 上執行
  • 阻塞狀态 / 睡眠狀态:暫時不可以去CPU上執行

Linux中的進程狀态還有很多其他的…

8、優先級
先給誰分配時間,後給誰分配時間,以及給誰分的多,給誰分的少…

9、記賬信息
統計了每個進程,都分别被執行了多久,分别都執行了哪些指令,分别都排隊等了多久了…
給進程調度提供指導依據的

10、上下文
就表示了上次進程被調度出 CPU 的時候,當時程序的執行狀态。下次進程上CPU的時候,就可以恢複之前的狀态,然後繼續往下執行

進程被調度出CPU之前,要先把CPU中的所有的寄存器中的數據都給保存到内存中 (PCB的上下文字段中) ,相當于存檔了
下次進程再被調度上CPU的時候,就可以從剛才的内存中恢複這些數據到寄存器中,相當于讀檔了

存檔+讀檔,存檔存儲的遊戲信息,就稱為 “上下文”


5、内存分配 —— 内存管理(Memory Manage)

進程的調度,其實就是操作系統在考慮CPU資源如何給各個進程分配
那内存資源又是如何分配的呢?

11、虛拟地址空間:
由于操作系統上,同時運行着很多個進程,如果某個進程,出現了bug 進程崩潰了,是否會影響到其他進程呢?
現代的操作系統 (windows, linux, mac… ) ,能夠做到這一點,就是 “進程的獨立性” 來保證的,就依仗了"虛拟地址空間"

例:如果某個居民核酸變成陽性了,是否會影響到其他的居民呢?
一旦發現有人陽性了,就需要立刻封樓封小區,否則就會導緻其他人也被傳染,

這個情況就類似于早期的操作系統,早期的操作系統,裡面的進程都是訪問同一個内存的地址空間。如果某個進程出現 bug,把某個内存的數據給寫錯了,就可能引起其他進程的崩潰

解決方案,就是把這個院子,給劃分出很多的道路
這些道路之間彼此隔離開,每個人走各自的道理,這個時候就沒事了,此時即使有人确診,也影響不到别人了,

如果把進程按照虛拟地址空間的方式給劃分出了很多份,這個時候不是每一份就隻剩一點了嘛?? 雖然你的系統有百八十個進程,但是實際上從微觀上看,同時執行的進程,就6個!!
每個進程能夠撈着的内存還是挺多的,而且另一方面,也不是所有的進程都用那麼多的内存,有的進程 (一個3A遊戲,吃幾個G),大多數的進程也就隻占幾M即可


6、進程間通信(Inter Process Communication)

12、進程間通信
進程之間現在通過虛拟地址空間,已經各自隔離開了,但是在實際工作中,進程之間有的時候還是需要相互交互的。

例:某業主A問:兄弟們,誰家有土豆,借我兩個
業主B回答:我有土豆,我給你
設定一個公共空間,這個空間是任何居民都可以來訪問的,
讓B先把土豆放到公共空間中,進行消毒,再讓A來把這個公共空間的土豆給取走,彼此就不容易發生傳染

類似的,咱們的兩個進程之間,也是隔離開的,也是不能直接交互的,操作系統也是提供了類似的 "公共空間”,
進程 A 就可以把數據見放到公共空間上,進程B再取走

進程間通信:
操作系統中,提供的 “公共空間” 有很多種,并且各有特點,有的存儲空間大,有的小,有的速度快,有的慢.….
操作系統中提供了多種這樣的進程間通信機制,(有些機制是屬于曆史遺留的,已經不适合于現代的程序開發)

現在最主要使用的進程間通信方式兩種:
1.文件操作
2.網絡操作 (socket)

總結:


四、多線程 1、線程(Thread)

為啥要有進程?因為我們的系統支持多任務了,程序猿也就需要 “并發編程”
通過多進程,是完全可以實現并發編程的,但是有點小問題:
如果需要頻繁的創建而 / 銷毀進程,這個事情成本是比較高的,如果需要頻繁的調度進程,這個事情成本也是比較高的:
對于資源的申請和放,本身就是一個比較低效的操作,

創建進程就得分配資源:
1)内存
2)文件
銷毀進程也得釋放資源
1)内存
2)文件

如何解決這個問題?思路有兩個:

  1. 進程池: (如數據庫連接池,字符串常量池)
    進程池雖然能解決上述問題,提高效率。同時也有問題:池子裡的閑置進程,不使用的時候也在消耗系統資源,消耗的系統資源太多了
  2. 使用線程來實現并發編程:
    線程比進程更輕量,每個進程可以執行一個任務,每個線程也能執行一個任務 (執行一段代碼),也能夠并發編程,
    創建線程的成本比創建進程要低很多。銷毀線程,的成本也比銷毀進程低很多。調度線程,的成本也比調度進程低很多。
    在 Linux 上也把線程稱為輕量級進程(LWP light weight process)

2、為什麼線程比進程更輕量?
  1. 進程重量是重在哪裡:重在資源申請釋放 (在倉庫裡找東西…)
  2. 線程是包含在進程中的,一個進程中的多個線程,共用同一份資源 (同一份内存+文件)
    隻是創建進程的第一個線程的時候 (由于要分配資源)成本是相對高的,後續這個進程中再創建其他線程,這個時候成本都是要更低一些,所以為什麼更輕量?少了申請釋放資源的過程

可以把進程比作一個工廠,假設這個工廠有一些生産任務,例如要生産 1w 部手機
要想提高生産效率:
1). 搞兩個工廠,一個生産 5k (多創建了一個進程)
2). 還是一個工廠,在一個工廠裡多加一個生産線,兩個生産線并行生産,一個生産線生産5k,(多創建了一個線程)
最終生産1w個手機,花的時間差不多,但是這裡的成本就不一樣了

多加一些線程,是不是效率就會進一步提高呢?一般來說是會,但是也不一定
如果線程多了,這些線程可能要競争同一個資源,這個時候,整體的速度就收到了限制,整體硬件資源是有限的

總結進程與線程的區别:

  1. 進程包含線程,一個進程裡可以有一個線程,也可以有多個線程
  2. 進程和線程都是為了處理 并發編程 這樣的場景
    但是進程有問題,頻繁創建和釋放的時候效率低,相比之下,線程更輕量,創建和釋放效率更高。為啥更輕量?少了申請釋放資源的過程
  3. 操作系統創建進程,要給進程分配資源,進程是操作系統分配資源的基本單位
    操作系統創建的線程,是要在 CPU上調度執行,線程是操作系統調度執行的基本單位,(前面講的時間管理,當時咱們是調度的進程,但是更準确的說,其實是調度的線程)
    • 調度的進程:前面的例子相當于是每個進程裡,隻有一個線程了,可以視為是在調度進程,但是如果進程裡有多個線程,更嚴謹的說法,還是以線程為單位進行調度
  4. 進程具有獨立性,每個進程有各自的虛拟地址空間,一個進程挂了,不會影響到其他進程。
    同一個進程中的多個線程,共用同一個内存空間,一個線程挂了,可能影響到其他線程的,甚至導緻整個進程崩潰

Java這個生态中更常使用的并發編程方式,是多線程
其他的語言,主打的并發變成又不一樣:
go,主要是通過多協程的方式實現并發.
erlang,這個是通過 actor 模型實現并發.
JS,是通過定時器+事件回調的方式實現并發.……
多線程仍然是最主流最常見的一種并發編程的方式


五、Java 多線程編程

Java 的線程 和 操作系統線程 的關系:

  1. 線程是操作系統中的概念,操作系統内核實現了線程這樣的機制,并且對用戶層提供了一些 API 供用戶使用 (例如 Linux 的 pthread 庫),
    在 Java 标準庫 中,就提供了一個 Thread 類,來表示 / 操作線程,Thread 類可以視為 Java 标準庫提供的 API, 對操作系統提供的 API 進行了進一步的抽象和封裝
  2. 創建好的 Thread實例,其實和操作系統中的線程是一 一對應的關系,操作系統提供了一組關于線程的API(C語言風格),Java對于這組API進一步封裝了,就成了Thread 類
1、第一個多線程程序

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 之前,系統中是沒有創建出線程的


2、線程之間是并發執行的

如果在一個循環中不加任何限制,這個循環轉的速度非常非常快,導緻打印的東西太多了,根本看不過來了,就可以加上一個 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,這是不确定的 (随機的)
操作系統來說,内部對于線程之間的調度順序,在宏觀上可以認為是随機的 (搶占式執行)
這個随機性,會給多線程編程帶來很多其他的麻煩


3、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類的其他的屬性和方法 1、Thread 的常見構造方法
方法 說明
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自帶的一個調試工具


jconsole 這裡能夠羅列出你系統上的java進程(其他進程不行)




2、Thread 的幾個常見屬性
屬性 獲取方法
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());
    }
}

3、Thread中的一些重要方法 3.1、啟動一個線程-start()

start() 決定了系統中是不是真的創建出線程

start 和 run 的區别:

  • run() 單純的隻是一個普通的方法,描述了任務的内容
  • start() 則是一個特殊的方法,内部會在系統中創建線程
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


3.2、中斷一個線程

中斷線程:讓一個線程停下來

線程停下來的關鍵,是要讓線程對應的 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() 判斷對象關聯的線程的标志位是否設置,調用後不清除标志位

3.3、線程等待-join()

多個線程之間,調度順序是不确定的,線程之間的執行是按照調度器來安排的,這個過程可以視為是 “無序,随機”,這樣不太好,有些時候,我們需要能夠控制線程之間的順序

線程等待,就是其中一種控制線程執行順序的手段
此處的線程等待,主要是控制線程結束的先後順序

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 也就直接返回
日常開發中涉及到的一些 "等待” 相關的操作,一般都不會是死等,而是會有這樣的 "超時時間"


3.4、獲取當前線程的引用

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();

3.5、休眠當前線程

sleep 所謂的休眠到底是在幹啥?
進程:PCB+雙向鍊表,這個說法是針對隻有一個線程的進程是如此的
如果是一個進程有多個線程,此時每個線程都有一個PCB,一個進程對應的就是一組PCB了
PCB 上有一個字段tgroupld,這個id其實就相當于進程的id,同一個進程中的若幹個線程的 tgroupld 是相同的

process control block
進程控制塊 和 線程有啥關系?其實 Linux内核不區分進程和線程
進程線程是程序猿寫應用程序代碼,搞出來的詞,實際上 Linux内核隻認PCB !
在内核裡 Linux 把線程稱為輕量級進程


如果某個線程調用了sleep 方法,這個 PCB 就會進入到阻塞隊列

操作系統調度線程的時候,就隻是從就緒隊列中挑選合适的 PCB 到 CPU 上運行,阻塞隊列裡的 PCB 就隻能幹等着,當睡眠時間到了,系統就會把剛才這個 PCB 從阻塞隊列挪回到就緒隊列,以上情況都是在 Linux 系統

内核中的很多工作都依賴大量的數據結構,但凡是需要管理很多數據的程序,都大量的依賴數據結構


4、線程的狀态

進程有狀态:就緒,阻塞
這裡的狀态就決定了系統按照啥樣的态度來調度這個進程,這裡相當于是針對一個進程中隻有一個線程的情況
更常見的情況下,一個進程中包含了多個線程,所謂的狀态,其實是綁定在線程上
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

為啥要這麼細分?這是非常有好處的:
開發過程中經常會遇到一種情況,程序 "卡死” 了
一些關鍵的線程,阻塞了
在分析卡死原因的時候,第一步就可以先來看看當前程序裡的各種關鍵線程所處的狀态


5、線程狀态轉換簡圖

你可能想看:

有話要說...

取消
掃碼支持 支付碼