行程與執行緒
1 行程
1.1 行程的概念
行程就是正在運行的程式,它會占用對應的記憶體區域,由CPU進行執行與計算,
1.2 行程的特點
- 獨立性
行程是系統中獨立存在的物體,它可以擁有自己獨立的資源,每個行程都擁有自己私有的地址空間,在沒有經過行程本身允許的情況下,一個用戶行程不可以直接訪問其他行程的地址空間 - 動態性
行程與程式的區別在于,程式只是一個靜態的指令集合,而行程是一個正在系統中活動的指令集合,程式加入了時間的概念以后,稱為行程,具有自己的生命周期和各種不同的狀態,這些概念都是程式所不具備的. - 并發性
多個行程可以在單個處理器CPU上并發執行,多個行程之間不會互相影響.
2 執行緒
2.1 執行緒的概念
執行緒是作業系統OS能夠進行運算調度的最小單位,它被包含在行程之中,是行程中的實際運作單位.
一個行程可以開啟多個執行緒,其中有一個主執行緒來呼叫本行程中的其他執行緒,
我們看到的行程的切換,切換的也是不同行程的主執行緒
多執行緒可以讓同一個行程同時并發處理多個任務,相當于擴展了行程的功能,
2.2 行程與執行緒的關系
一個作業系統中可以有多個行程,一個行程中可以包含一個執行緒(單執行緒程式),也可以包含多個執行緒(多執行緒程式)

每個執行緒在共享同一個行程中的記憶體的同時,又有自己獨立的記憶體空間.
所以想使用執行緒技術,得先有行程,行程的創建是OS作業系統來創建的,一般都是C或者C++完成

3 多執行緒的特性
3.1 隨機性
我們宏觀上覺得多個行程是同時運行的,但實際的微觀層面上,一個CPU【單核】只能執行一個行程中的一個執行緒,
那為什么看起來像是多個行程同時執行呢?
是因為CPU以納秒級別甚至是更快的速度高效切換著,超過了人的反應速度,這使得各個行程從看起來是同時進行的,也就是說,宏觀層面上,所有的行程看似并行【同時運行】,但是微觀層面上是串行的【同一時刻,一個CPU只能處理一件事】,

串行與并行
串行是指同一時刻一個CPU只能處理一件事,類似于單車道
并行是指同一時刻多個CPU可以處理多件事,類似于多車道


3.2 CPU分時調度
時間片,即CPU分配給各個執行緒的一個時間段,稱作它的時間片,即該執行緒被允許運行的時間,如果在時間片用完時執行緒還在執行,那CPU將被剝奪并分配給另一個執行緒,將當前執行緒掛起,如果執行緒在時間片用完之前阻塞或結束,則CPU當即進行切換,從而避免CPU資源浪費,當再次切換到之前掛起的執行緒,恢復現場,繼續執行,
注意:我們無法控制OS選擇執行哪些執行緒,OS底層有自己規則,如:
- FCFS(First Come First Service 先來先服務演算法)
- SJS(Short Job Service短服務演算法)

3.3 執行緒的狀態
由于執行緒狀態比較復雜,我們由易到難,先學習執行緒的三種基礎狀態及其轉換,簡稱”三態模型” :
- 就緒(可運行)狀態:執行緒已經準備好運行,只要獲得CPU,就可立即執行
- 執行(運行)狀態:執行緒已經獲得CPU,其程式正在運行的狀態
- 阻塞狀態:正在運行的執行緒由于某些事件(I/O請求等)暫時無法執行的狀態,即執行緒執行阻塞

就緒 → 執行:為就緒執行緒分配CPU即可變為執行狀態"
執行 → 就緒:正在執行的執行緒由于時間片用完被剝奪CPU暫停執行,就變為就緒狀態
執行 → 阻塞:由于發生某事件,使正在執行的執行緒受阻,無法執行,則由執行變為阻塞
(例如執行緒正在訪問臨界資源,而資源正在被其他執行緒訪問)
反之,如果獲得了之前需要的資源,則由阻塞變為就緒狀態,等待分配CPU再次執行
我們可以再添加兩種狀態:
- 創建狀態:執行緒的創建比較復雜,需要先申請PCB,然后為該執行緒運行分配必須的資源,并將該執行緒轉為就緒狀態插入到就緒佇列中
- 終止狀態:等待OS進行善后處理,最后將PCB清零,并將PCB回傳給系統

PCB(Process Control Block):為了保證參與并發執行的每個執行緒都能獨立運行,OS配置了特有的資料結構PCB來描述執行緒的基本情況和活動程序,進而控制和管理執行緒
3.4 執行緒狀態與代碼對照

執行緒生命周期,主要有五種狀態:
- 新建狀態(New) : 當執行緒物件創建后就進入了新建狀態.如:Thread t = new MyThread();
- 就緒狀態(Runnable):當呼叫執行緒物件的start()方法,執行緒即為進入就緒狀態.
處于就緒(可運行)狀態的執行緒,只是說明執行緒已經做好準備,隨時等待CPU調度執行,并不是執行了t.start()此執行緒立即就會執行 - 運行狀態(Running):當CPU調度了處于就緒狀態的執行緒時,此執行緒才是真正的執行,即進入到運行狀態
就緒狀態是進入運行狀態的唯一入口,也就是執行緒想要進入運行狀態狀態執行,先得處于就緒狀態 - 阻塞狀態(Blocked):處于運狀態中的執行緒由于某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入就緒狀態才有機會被CPU選中再次執行.
根據阻塞狀態產生的原因不同,阻塞狀態又可以細分成三種:
等待阻塞:運行狀態中的執行緒執行wait()方法,本執行緒進入到等待阻塞狀態
同步阻塞:執行緒在獲取synchronized同步鎖失敗(因為鎖被其他執行緒占用),它會進入同步阻塞狀態
其他阻塞:呼叫執行緒的sleep()或者join()或發出了I/O請求時,執行緒會進入到阻塞狀態.當sleep()狀態超時.join()等待執行緒終止或者超時或者I/O處理完畢時執行緒重新轉入就緒狀態 - 死亡狀態(Dead):執行緒執行完了或者因例外退出了run()方法,該執行緒結束生命周期
4 多執行緒代碼創建方式1:繼承Thread
4.1 概述
Thread類本質上是實作了Runnable介面的一個實體,代表一個執行緒的實體
啟動執行緒的唯一方法就是通過Thread類的start()實體方法
start()方法是一native方法,它將通知底層作業系統,.最終由作業系統啟動一個新執行緒,作業系統將執行run()
這種方式實作的多執行緒很簡單,通過自己的類直接extends Thread,并重寫run()方法,就可以自動啟動新執行緒并執行自己定義的run()方法
模擬開啟多個執行緒,每個執行緒呼叫run()方法.
4.2 常用方法
構造方法
Thread() 分配新的Thread物件
Thread(String name) 分配新的Thread物件
Thread(Runnable target) 分配新的Thread物件
Thread(Runnable target,String name) 分配新的Thread物件
普通方法
static Thread currentThread( )
回傳對當前正在執行的執行緒物件的參考
long getId()
回傳該執行緒的標識
String getName()
回傳該執行緒的名稱
void run()
如果該執行緒是使用獨立的 Runnable 運行物件構造的,則呼叫該 Runnable 物件的 run 方法
static void sleep(long millions)
在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行)
void start()
使該執行緒開始執行:Java虛擬機呼叫該執行緒的run()
4.3 測驗多執行緒的創建方式1
創建包: cn.tedu.thread
創建類: TestThread1.java
package cn.tedu.thread;
/*本類用于多執行緒編程實作方案一:繼承Thread類來完成*/
public class TestThread1 {
public static void main(String[] args) {
//4.創建執行緒物件進行測驗
/*4.new對應的是執行緒的新建狀態
* 5.要想模擬多執行緒,至少得啟動2個執行緒,如果只啟動1個,是單執行緒程式*/
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
MyThread t4 = new MyThread();
/*6.這個run()如果直接這樣呼叫,是沒有多執行緒搶占執行的效果的
* 只是把這兩句話看作普通方法的呼叫,誰先寫,就先執行誰*/
//t1.run();
//t2.run();
/*7.start()對應的狀態就是就緒狀態,會把剛剛新建好的執行緒加入到就緒佇列之中
* 至于什么時候執行,就是多執行緒執行的效果,需要等待OS選中分配CPU
* 8.執行的時候start()底層會自動呼叫我們重寫的run()種的業務
* 9.執行緒的執行具有隨機性,也就是說t1-t4具體怎么執行
* 取決于CPU的調度時間片的分配,我們是決定不了的*/
t1.start();//以多執行緒的方式啟動執行緒1,將當前執行緒變為就緒狀態
t2.start();//以多執行緒的方式啟動執行緒2,將當前執行緒變為就緒狀態
t3.start();//以多執行緒的方式啟動執行緒3,將當前執行緒變為就緒狀態
t4.start();//以多執行緒的方式啟動執行緒4,將當前執行緒變為就緒狀態
}
}
//1.自定義一個多執行緒類,然后讓這個類繼承Thread
class MyThread extends Thread{
/*1.多執行緒編程實作的方案1:通過繼承Thread類并重寫run()來完成的 */
//2.重寫run(),run()里是我們自己的業務
@Override
public void run() {
/*2.super.run()表示的是呼叫父類的業務,我們現在要用自己的業務,所以注釋掉*/
//super.run();
//3.完成業務:列印10次當前正在執行的執行緒的名稱
for (int i = 0; i < 10; i++) {
/*3.getName()表示可以獲取當前正在執行的執行緒名稱
* 由于本類繼承了Thread類,所以可以直接使用這個方法*/
System.out.println(i+"="+getName());
}
}
}
5 多執行緒代碼創建方式2:實作Runnable介面
5.1 概述
如果自己的類已經extends另一個類,就無法多繼承,此時,可以實作一個Runnable介面
5.2 常用方法
void run()使用實作介面Runnable的物件創建執行緒時,啟動該執行緒將導致在獨立執行的執行緒中呼叫物件的run()方法
5.3 練習2:測驗多執行緒的創建方式2
創建包: cn.tedu.thread
創建類: Thread2.java
package cn.tedu.thread;
/*本類用于多執行緒編程實作方案二:實作Runnable介面來完成*/
public class TestThread2 {
public static void main(String[] args) {
//5.創建自定義類的物件--目標業務類物件
MyRunnable target = new MyRunnable();
//6.如何啟動執行緒?自己沒有,需要與Thread建立關系
Thread t1 = new Thread(target);
Thread t2 = new Thread(target);
Thread t3 = new Thread(target);
Thread t4 = new Thread(target);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
//1.自定義多執行緒類
class MyRunnable implements Runnable{
//2.添加父介面中的抽象方法run(),里面是自己的業務
@Override
public void run() {
//3.寫業務,列印10次當前正在執行的執行緒名稱
for (int i = 0; i < 10; i++) {
/*問題:自定義類與父介面Runnable中都沒有獲取名字的方法
* 所以還需要從Thread中找:
* currentThread():靜態方法,獲取當前正在執行的執行緒物件
* getName():獲取當前執行緒的名稱*/
System.out.println(i+"="+Thread.currentThread().getName());
}
}
}
5.4 兩種實作方式的比較
- 繼承Thread類
優點: 撰寫簡單,如果需要訪問當前執行緒,無需使用Thread.currentThread()方法,直接使用this即可獲得當前執行緒
缺點: 自定義的執行緒類已繼承了Thread類,所以后續無法再繼承其他的類 - 實作Runnable介面
優點: 自定義的執行緒類只是實作了Runnable介面或Callable介面,后續還可以繼承其他類,在這種方式下,多個執行緒可以共享同一個target物件,所以非常適合多個相同執行緒來處理同一份資源的情況,從而可以將CPU、代碼、還有資料分開(解耦),形成清晰的模型,較好地體現了面向物件的思想
缺點: 編程稍微復雜,如想訪問當前執行緒,則需使用Thread.currentThread()方法
6 售票案例
需求:設計4個售票視窗,總計售票100張,用多執行緒的程式設計并寫出代碼
6.1 方案1:繼承Thread
創建包: cn.tedu.tickets
創建類: TestThread.java
package cn.tedu.tickets;
/*需求:設計多執行緒編程模型,4個視窗共計售票100張
* 本方案使用多執行緒編程方案1,繼承Thread類的方式來完成*/
public class TestThread {
public static void main(String[] args) {
//5.創建多個執行緒物件
TicketThread t1 = new TicketThread();
TicketThread t2 = new TicketThread();
TicketThread t3 = new TicketThread();
TicketThread t4 = new TicketThread();
//6.以多執行緒的方式啟動
t1.start();
t2.start();
t3.start();
t4.start();
}
}
//1.自定義多執行緒售票類,繼承Thread
class TicketThread extends Thread{
//3.定義變數,保存要售賣的票數
/*問題:4個執行緒物件共計售票400張,原因是創建了4次物件,各自操作各自的成員變數
* 解決:讓所有物件共享同一個資料,票數需要設定為靜態*/
static int tickets = 100;
//2.重寫父類的run(),里面是我們的業務
@Override
public void run() {
//4.1回圈賣票
while(true){
try {
//7.讓每個執行緒經歷休眠,增加執行緒狀態切換的頻率與出錯的概率
//問題1:產生了重賣的現象:同一張票賣了多個人
//問題2:產生了超賣的現象:超出了規定的票數100,出現了0 -1 -2這樣的票
Thread.sleep(10);//讓當前執行緒休眠10ms
} catch (InterruptedException e) {
e.printStackTrace();
}
//4.2列印當前正在賣票的執行緒名稱,并且票數-1
System.out.println(getName()+"="+tickets--);
//4.3做判斷,如果沒有票了,就退出死回圈
if(tickets <= 0) break;//注意,死回圈一定要設定出口
}
}
}
6.2 方案2:實作Runnable
創建包: cn.tedu.tickets
創建類: TestRunnable.java
package cn.tedu.tickets;
/*需求:設計多執行緒編程模型,4個視窗共計售票100張
* 本方案使用多執行緒編程方案2,實作Runnable介面的方式來完成*/
public class TestRunnable {
public static void main(String[] args) {
//5.創建Runnable介面的實作類物件,作為目標業務物件
TicketRunnable target = new TicketRunnable();
//6.創建多個Thread類執行緒物件,并將target業務物件交給多個執行緒物件來處理
Thread t1 = new Thread(target);
Thread t2 = new Thread(target);
Thread t3 = new Thread(target);
Thread t4 = new Thread(target);
//7.以多執行緒的方式啟動多個執行緒物件
t1.start();
t2.start();
t3.start();
t4.start();
}
}
//1.自定義多執行緒類實作Runnable介面
class TicketRunnable implements Runnable{
//3.定義一個成員變數,用來保存票數100
/*由于自定義類物件只創建了一次,所以票數被所有執行緒物件Thread類的物件共享*/
int tickets = 100;
//2.添加介面中未實作的方法,方法里是我們的業務
@Override
public void run() {
//4.1回圈賣票
while(true){
//8.讓執行緒休眠10ms,增加執行緒狀態切換的概率和出錯的概率
try {
Thread.sleep(10);//讓當前執行緒休眠10ms
} catch (InterruptedException e) {
e.printStackTrace();
}
//4.2列印當前正在售票的執行緒名稱 & 票數-1
System.out.println(Thread.currentThread().getName()+"="+tickets--);
//4.3設定死回圈的出口,沒票了就停止賣票
if(tickets <=0 ) break;
}
}
}
6.3 問題
- 每次創建執行緒物件,都會生成一個tickets變數值是100,創建4次物件就生成了400張票了,不符合需求,怎么解決呢?能不能把tickets變數在每個物件間共享,就保證多少個物件都是賣這100張票,
解決方案: 用靜態修飾 - 產生超賣,0 張 、-1張、-2張,
- 產生重賣,同一張票賣給多人,
- 多執行緒安全問題是如何出現的?常見情況是由于執行緒的隨機性+訪問延遲,
- 以后如何判斷程式有沒有執行緒安全問題?
在多執行緒程式中 + 有共享資料 + 多條陳述句操作共享資料
解決方案:下一節 同步鎖點這里
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/389022.html
標籤:其他
上一篇:中國DevOps社區峰會 2021·深圳——我的識訓與作業要點
下一篇:Nginx:Nginx熱部署
