1. 程式人生 > >Java多執行緒和記憶體模型(一):程序和執行緒基礎

Java多執行緒和記憶體模型(一):程序和執行緒基礎

Java多執行緒和記憶體模型(一)

由於java是執行在 JVM上 的,所以需要涉及到 JVM 的記憶體模型概念,需要理解記憶體模型,就需要多執行緒的基礎;
而執行緒是基於載體執行緒裡的,所以我們藉由作業系統的程序來講一講。

程序

什麼是程序?

  • 程序是程式的執行例項
  • 程序是一個程式及其資料在處理機上順序執行時所發生的活動
  • 程序本身不是基本執行單位,而是執行緒的容器
  • 但程序是系統進行資源分配和排程的一個單位
  • 程序需要一些資源才能完成工作,如CPU使用時間、記憶體、檔案以及I/O裝置,且為依序逐一進行,也就是每個CPU核心任何時間內僅能執行一項程序。
  • 我們常說的程序實體就是PCB塊(Process Control Block)

什麼是PCB(程序控制塊)?

程序控制塊(PCB)是系統為了管理程序設定的一個專門的資料結構。 系統用它來記錄程序的外部特徵,描述程序的運動變化過程。 同時,系統可以利用PCB來控制和管理程序;

PCB(程序控制塊)是系統感知程序存在的唯一標誌。

通常我們編寫的程式是靜止的東西,是不能併發執行的個體,為了使程式(包含資料)能獨立執行,便出現了為程式配備PCB(程序控制塊)的概念;
由程式段,相關資料段和PCB三部分構成了程序實體

程序的特徵

1.動態性

程序擁有自己的生命週期,它由建立而產生,由排程而執行,由撤銷而消亡;
容易模糊的概念:程式是不是一個程序?

  • 程式本身只是指令、資料及其組織形式的描述,並且存放在某種儲存介質上,屬於靜態的
  • 程序才是程式(那些指令和資料)的真正執行例項。
2.併發性
3.非同步性

程序是按照非同步方式執行的,即按各自獨立的,不可預知的速度向前推進。若參與併發執行,產生的結果會出現不可再現性。如果需要保持其結果是可再現的,需要配合相應的程序同步機制進行控制。

4.獨立性

在傳統的OS中,程序實體是一個能獨立執行的,獨立獲得資源和獨立接受排程的基本單位;凡未建立PCB的程式都不能作為一個獨立的單位參與執行。

程序的三種基本狀態

1.就緒態(Ready)
該狀態的程序已分配除CPU以外的所有必要資源後,只需要獲得CPU,便可立即執行。
所以就緒的前提是執行所需的資源分配完全。

2.執行態(Running)
程序已獲得CPU,其程式正在執行。在單處理機系統中,只有一個程序處於執行狀態; 在多處理機系統中,則有多個程序處於執行狀態。

3.阻塞態(Block)
正在執行的程序由於發生某事件而暫時無法繼續執行時,便放棄處理機而處於暫停狀態,亦即程序的執行受到阻塞,把這種暫停狀態稱為阻塞狀態,有時也稱為等待狀態或封鎖狀態。
導致程序阻塞的典型事件:

  • I/O請求
  • 申請緩衝區
  • 等待信件(訊號)等

通常將這種處於阻塞狀態的程序也排成一個佇列。有的系統則根據阻塞原因的不同而把處於阻塞狀態的程序排成多個佇列。
Alt text

現在的作業系統中的程序除了以上的三種基本狀態,還多了一種 ”掛起狀態“

什麼是掛起狀態?
程序在作業系統中可以定義為暫時被淘汰出記憶體的程序,機器的資源是有限的,在資源不足的情況下,作業系統對在記憶體中的程式進行合理的安排,其中有的程序被暫時調離出記憶體,當條件允許的時候,會被作業系統再次調回記憶體,重新進入等待被執行的狀態即就緒態,系統在超過一定的時間沒有任何動作。
掛起在作業系統中用原語表示為:Suspend ;相應的啟用原語為:Active

掛起和阻塞的區別:
掛起為主動掛起,阻塞是被動阻塞;

掛起產生的五個原因:
1.交換(負荷調節需求):
作業系統需要釋放足夠的記憶體空間,以調入並執行處於就緒狀態的程序。

2.其他OS原因:
作業系統可能掛起後臺程序或工具程式程序,或者被懷疑導致問題的程序。

3.互動式使用者請求:
使用者可能希望掛起一個程式的執行,目的是為了除錯(debug)或與一個資源的使用進行連線。

4.定時:
一個程序可能會週期性地執行(例如記賬或系統監視程序),而且可能再等待下一個時間間隔時被掛起。

5.父程序請求:
父程序可能會希望掛起後代程序的執行,以檢查或修改掛起的程序,或者協調不同後代程序之間的行為。

加入掛起後的程序狀態轉換圖:
Alt text

靜止阻塞的時候,要是當時被阻塞的程序的需求者來到,那麼它就會被釋放到靜止就緒狀態;但是沒有獲得Active() 啟用狀態下,它依舊沒辦法進入活動就緒佇列,也就沒有辦法得到執行

程序既然具有生命週期,必然存在建立和終止的狀態

1.建立狀態:

  • 申請空的PCB
  • 向PCB中寫入控制和管理程序的資訊
  • 分配執行時該程序所需要的資源
  • 把該程序裝入就緒狀態並插入就緒佇列中

2.終止狀態

  • 由作業系統進行終止程序的善後工作(主要是儲存狀態碼和一些計時統計資料)
  • 善後工作完成後,將PCB清0,並把PCB所佔空間返回給系統

建立程序的實質:建立程序實體中的PCB
撤銷程序的實質:撤銷程序的PCB

需要注意的經典程序同步問題

1.生產者–消費者問題

2.哲學家進餐問題

3.讀者–寫者問題

執行緒

什麼是執行緒?

執行緒是作業系統能夠進行運算排程的最小單位;它被包含在程序中,是程序中的實際運作單位。
一條執行緒指的是程序中一個單一順序的控制流,一個程序中可以併發多個執行緒,每條執行緒並行執行不同的任務。在Unix System V 及 SunOS 中也被稱為輕量程序(lightweight processes),但是輕量程序更多指核心執行緒(kernel thread),把使用者執行緒(user thread) 稱為執行緒。

執行緒與程序的關係

  • 同一個程序中的多條執行緒將共享該程序中的全部系統資源,如虛擬地址空間,檔案描述符號 和 訊號處理等;
  • 同一程序中的多個執行緒有各自的呼叫棧,暫存器環境,執行緒本地儲存
  • 區別:
    • 排程層面:
    • 在引入執行緒的作業系統中,把執行緒作為排程和分派的基本單位,而程序作為資源擁有的基本單位;
    • 在同一個程序中,執行緒的切換不會引起程序的切換;
    • 從一個程序中的執行緒切換到另一個程序中的執行緒時,會引起程序的切換;
    • 併發性層面:
    • 程序之間在併發執行的同時,程序內的若干執行緒也可以併發執行;
    • 擁有資源:
    • 程序擁有資源;執行緒不擁有資源,共享所屬程序的資源;
    • 系統開銷:
    • 程序在被建立和撤銷時,建立和回收PCB,分配和回收資源,系統的開銷明顯高與執行緒;
    • 程序切換時,CPU環境的保護以及新被排程的程序CPU環境的配置;而執行緒的切換僅需要儲存和設計少量暫存器內容;
    • 通訊方面,一個程序中的所有執行緒具有相同的地址空間,在同步和通訊的實現方面執行緒也比程序容易;

Java中的執行緒

生命週期

java執行緒在執行的生命週期會有6種:

狀態名稱 說明
NEW(new) 初始狀態,執行緒被構建,但是還沒有呼叫Star()方法
RUNNABLE(runnable) 執行狀態,Java執行緒將作業系統中的就緒(Ready)和執行(Runing)兩種狀態籠統的稱為“執行中”
BLOCKED(blocked) 阻塞狀態,表示執行緒阻塞於鎖
WAITING(waiting) 等待狀態,表示執行緒進入等待狀態,進入該狀態表示當前執行緒需要等待其他執行緒做出一些特定的動作(通知或者中斷)
TIME_WAITING(time_waiting) 超時等待狀態,該狀態不用與WAITING混淆,它是可以在指定的時間自行返回的
TREMINATED(treminated) 終止狀態,表示當前執行緒已經執行完畢

Java執行緒生命週期流程圖

Alt text

Java執行緒常用方法

方法 描述
Start方法 start()用來啟動一個執行緒,當呼叫start方法後,系統才會開啟一個新的執行緒來執行使用者定義的子任務,在這個過程中,會相應的執行緒分配需要的資源。
runf法 run()方法是不需要永固來呼叫的,當通過start()方法啟動一個執行緒之後,當執行緒獲得了CPU執行時間,便進入run方法體中去執行具體的任務。注意:繼承Thread類必須重寫run方法,在run方法中定義具體要執行的任務
sleep方法 sleep相當於讓執行緒睡眠,交出CPU的佔有權,讓CPU去執行其他任務,(進入阻塞佇列)sleep方法不會釋放鎖,也就是說如果當前執行緒持有對某個物件的鎖,則即使呼叫sleep方法,其他執行緒也無法訪問到這個物件
yield方法 yield方法會讓當前執行緒交出CPU許可權,讓CPU去執行其他執行緒。它和Sleep方法類似,同樣不會釋放鎖。但是yield不能控制具體的交出CPU的時間,另外,yield方法只能讓擁有相同優先順序的執行緒獲取CPU執行時間的機會。呼叫yield方法並不會讓執行緒進入阻塞狀態,而是讓執行緒重回就緒的狀態,它只需要等待重新獲取CPU執行時間,只是和sleep方法的區別
join方法 join方法就是往執行緒中新增東西;join方法可以用於臨時加入執行緒,例如:在一個執行緒運算過程中,若滿足條件,可以臨時加入一個執行緒,讓這個執行緒運算完,另一個執行緒再繼續執行。
interrupt方法 作中斷用,單獨呼叫interrupt方法可以使處於阻塞狀態的執行緒丟擲一個異常,也就是說,它可以用來中斷一個正處於阻塞狀態的執行緒;直接呼叫interrupt不能直接中斷正在執行的執行緒,需要用interrupt方法和isInterrupt方法來停止正在執行的執行緒,一般通過增加一個屬性isStop來標誌是否結束while迴圈。
suspend(),resume()和stop()方法(已經過期) 這三個方法分別實現了執行緒的暫停,恢復和終止工作;suspend方法在呼叫後,執行緒不會釋放已經佔有的資源(比如鎖),而是佔著資源進入睡眠狀態 ,這樣容易引發死鎖問題;而stop()方法在終結一個執行緒時不會保證該執行緒的資源正常釋放,通常是沒有給予執行緒完成資源釋放工作的機會,因此會導致程式可能工作在不確定狀態下。
getId 獲取執行緒ID
getName和setName 用來獲取和設定執行緒的名稱
getPriority和setPriority 獲取和設定執行緒的優先順序
setDaemon和isDeamon 用來設定執行緒是否成為守護執行緒和判斷執行緒是否是守護執行緒;守護執行緒和使用者執行緒的區別:守護執行緒依賴於建立它的執行緒(也就是父執行緒),使用者執行緒就不依賴

流程圖對應ThreadState圖解:

Alt text

關於Suspend方法過期的替代方法:

需要用到suspend的地方基本都刻意替換為wait,notify模式;
如果你想要簡單實現,刻意考慮JUC提供的工具LockSupport。下面的例子使用LockSupport工具替換上面例子的suspend和resume方法。

長久以來對執行緒阻塞與喚醒經常我們會使用object的wait和notify,除了這種方式,java併發包還提供了另外一種方式對執行緒進行掛起和恢復,它就是併發包子包locks提供的LockSupport。

LockSupport是JDK中比較底層的類,用來建立鎖和其他同步工具類的基本執行緒阻塞原語。java鎖和同步器框架的核心AQS:AbstractQueuedSynchronizer,就是通過呼叫LockSupport.park()和LockSupport.unpark()實現執行緒的阻塞和喚醒的。

LockSupport是不可重入的,如果一個執行緒連續2次呼叫LockSupport.park(),那麼該執行緒一定會一直阻塞下去。

LockSupport很類似於二元訊號量(只有1個許可證可供使用),
park()獲取許可;
如果這個許可還沒有被佔用,當前執行緒獲取許可並繼續執行;
unpark()釋放許可;
如果許可已經被佔用,當前執行緒阻塞,等待獲取許可。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class SuspendTest2 {

    static Object lock = new Object();

    public static void main(String[] args) {
        Suspend s1 = new Suspend();
        Suspend s2 = new Suspend();
        s1.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LockSupport.unpark(s1);//釋放s1許可
        s2.start();
        LockSupport.unpark(s2);//釋放s2許可

    }

    static class Suspend extends Thread{
        @Override
        public void run() {
            synchronized(lock){
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                LockSupport.park();//獲取執行緒的許可
            }
        }
    }

}

未完待續。。。
謝謝關注!!