Java語言進階 #多線程 #Day17~18 #多線程 #多線程安全 #線程狀態 #線程通信 #線程池 #lambda

一、多線程安全

當多個線程之間,訪問了共享的數據,就會產生線程安全問題

1. Demo:賣票

我們以一個售票廳的例子舉例說明:

  1. 總共100張票,作爲共享數據
  2. 三個售票廳,一起售賣票,作爲三個線程

1.1 發生線程安全問題

在這裏插入圖片描述
在這裏插入圖片描述


結果:
在這裏插入圖片描述
在這裏插入圖片描述

會發現:

  1. 賣出了重複的票
  2. 賣出了不存在的票

1.2 線程安全問題原理

  1. 多個線程搶奪CPU的執行權
  2. 當執行到睡眠 時,線程把搶到的CPU執行權交出
  3. 當一個線程已經判斷完畢(無論如何,這部分函數體會執行),另一個線程改變了判斷條件也無濟於事,這時會出現條件外的值(不存在)
  4. 因爲多個線程共享資源,判斷依據相同,就會出現重複的情況

  • 總結: 所以,安全問題 的實質就是共享數據 時,一個線程讀時,另一個線程進行寫的操作,這個寫操作,改變之前線程讀的結果

所以,如果數據共享 ,但線程中只進行 ,是不會存在安全問題的。

  • 解決方法: 一個線程失去對CPU的控制權時,其它線程只能等待其執行完

2. 同步機制

同一時間,只能執行一件事,就叫同步
同步有三種方式:

  1. 同步代碼塊
  2. 同步方法
  3. 鎖機制

2.1 方法1:同步代碼塊

同步代碼塊 裏放可能會產生線程安全的代碼(訪問了共享數據的代碼),表示只對這個區塊的資源進行互斥訪問

synchronized(同步鎖){
	// 需要同步操作的代碼
}

注意:

  1. 同步代碼塊中的對象,可以是任意對象
  2. 多個線程的鎖對象 必須是同一個
  3. 鎖對象作用: 把同步代碼塊鎖住,只讓一個線程在同步代碼塊中執行

其它部分代碼不變,使用代碼塊包住需要鎖的部分
在這裏插入圖片描述

2.2 同步技術原理

使用了一個鎖對象 ,這個鎖對象叫同步鎖 ,也叫對象鎖、對象監視器

  1. t0搶到了CPU的執行權,執行run()方法,遇到synchronized代碼塊
  2. 檢查該代碼塊是否有鎖對象
  3. 如果有,就會獲取該鎖對象,進入代碼塊中執行
  4. t1搶到了CPU的執行權,執行run()方法,遇到synchronized代碼塊
  5. 檢查該代碼塊是否有鎖對象,發現沒有
  6. t1進入阻塞狀態,直到t0歸還鎖對象(t0要執行完同步代碼塊中內容,會把鎖對象歸還給同步代碼塊

總結: 同步中的線程,沒有執行完,不會釋放鎖。同步外的線程,沒有鎖,進不去同步。
同步保證了,只有一個線程在同步中執行共享數據,保證了安全
程序頻繁判斷鎖、獲取鎖、釋放鎖 ,程序的效率會降低

2.3 方法2:同步方法

把同步代碼塊中的內容,放在同步方法中,調用該方法即可

修飾符 synchronized 返回值類型 函數名() {}

例: public synchronized void method() {}
在這裏插入圖片描述
同步方法使用的鎖對象,就是Runnable的實現類對象,就是這個:
在這裏插入圖片描述

2.4 靜態同步方法

在同步方法前加static修飾,訪問的共享數據也用static修飾

// 訪問的必須是靜態共享
修飾符 static synchronized 返回值類型 函數名() {}

例: public static synchronized void method() {}
在這裏插入圖片描述
靜態同步方法使用的鎖對象,是本類的class屬性–>class文件對象(反射)
(之所以不用this, 因爲this是創建對象時產生的。靜態方法的創建優先於對象創建。)

2.5 方法3:Lock鎖

JDK1.5後有一個新的解決線程安全問題的方法,叫做Lock鎖
Lock接口,比 synchronized更加好用

  • 我們主要使用它的兩個方法void lock()獲取鎖void unlock()釋放鎖
  • 它有一個實現類ReentrankLock

步驟:

  1. 在成員位置,創建一個ReentrankLock對象
  2. 在可能出現線程安全的地方,獲取鎖
  3. 在這段代碼結束時,釋放鎖

在這裏插入圖片描述

提示: 最好把代碼放在try...catch中,並且把釋放鎖 放在finally中,這樣無論有沒有異常,都會釋放鎖。

3. 線程狀態

Thread中有一個嵌套類(內部類)Thread.State,記錄着線程的狀態
在這裏插入圖片描述
在這裏插入圖片描述

3.1 無限等待狀態

下面,以線程通信案例 來進行說明

  1. 創建一個線程,等待另一個線程的執行結果,調用wait();方法,進入無限等待狀態
  2. 另一個線程調用notify();,退出無限等待狀態

注意事項:

  1. 兩個線程必須得用同步代碼塊包裹起來,以保證等待和喚醒,只有一個在執行
  2. 同步使用的鎖對象,必須保證唯一
  3. 只有鎖對象才能調用wait()notify()(這兩個都是Object類中的方法)

兩個方法:

  1. wait() 在其它線程調用此對象的notify()和notifyall()方法前,將導致線程等待
  2. notify()喚醒在此對象監視器上等待的單個線程,會繼續執行wait方法之後的代碼

  • 代碼:
    在這裏插入圖片描述

  • 結果:在這裏插入圖片描述


注意: obj.wait()如果不加參數,就是無限等待。但它可以接收一個毫秒值做參數,等到時間走完,還沒有得到喚醒,會自動醒來 ,此時相當於Thread.sleep()方法

3.2 notifyAll

notify()如果有多個等待線程,隨機喚醒一個
notifyAll()如果有多個等待線程,全部喚醒
在這裏插入圖片描述
在這裏插入圖片描述
可以發現,一次只喚醒一個線程(隨機的)
notify()變爲notifyall()會得到如下結果

在這裏插入圖片描述

4. 線程間通訊

線程間通訊: 多個線程在處理同一資源,但線程的任務不同
多個線程操作同一份數據,就會發生數據的搶奪,通過等待喚醒機制 可以避免這種爭奪發生

注意: 被通知(notify)後,只是進入了可運行 狀態,不是立即執行的(因爲還在同步代碼塊那,還要搶鎖)

4.1 Demo:服務器返回一個網頁

分析:

  • 頁面數據: 請求,響應
  • 服務器: 接收請求,發送響應
  • 客戶端: 發送請求,接收響應

兩個線程的狀態是通信(互斥)狀態,必須保證兩個線程只有一個在執行

  1. 定義一個頁面準備的類,這個頁面需要文件,還需要資料渲染,還要請求響應狀態在這裏插入圖片描述

  2. 定義一個服務器:當有請求時,如果有響應,返回響應。如果沒響應,就進行響應。如果沒請求,就休息等待請求。
    在這裏插入圖片描述

  3. 定義一個客戶端:當無請求時,發送請求,當有請求時,請求響應。請求響應時,無限等待。
    在這裏插入圖片描述

  4. 定義一個運行用的主文件(我是用同級類的寫法)
    在這裏插入圖片描述

  5. 結果
    在這裏插入圖片描述

注:

  1. 可以在服務器和客戶端類中,添加死循環
  2. 現實業務中,服務器做的是溝通連接的工作,處理頁面的工作交給應用框架

二、線程池

線程池: 容納多個線程的容器,其中的線程可以複用,無需反覆創建線程而消耗過多的資源

1. 線程池原理

線程池是一個容器 ,也即一個集合(LinkedList<Thread>)

步驟:

  1. 程序開始的時候,創建多個線程,放進一個集合中
  2. 取出線程:Thread t = List.remove(index)Thread t = LinkedList.removeFirst()(線程只能被一個任務使用)
  3. 放回線程:List.add(t)LinkedList.addLast(t)

JDK1.5後,JDK內置了線程池,可以直接使用。


線程池的好處:

  1. 降低資源消耗: 減少了創建和銷燬線程的次數,使得線程可以複用。
  2. 提高響應速度: 因爲線程在任務開始前就已經創建好了,所以在任務開始時,可以立即響應。
  3. 提高可管理性: 一個線程所佔內存約爲1M,可以根據系統的承受能力,來限定線程數量

2. 線程池使用

在這裏插入圖片描述
java.util.concurrent.Executors是線程池的工廠類,用來生成線程池

static ExecutorService newFixedThreadPool(int 線程數目)

參數:線程數目

返回值:ExecutorService接口的實現類對象(可以使用接口類型來接收,這叫多態。這是一種【面向接口編程】)

java.util.concurrent.ExecutorService線程池接口


// 用來從線程池中獲取線程,調用start方法,執行線程任


submit(Runnable task)  // 提交一個Runnable任務用於執行

// 銷燬線程池(不建議執行)

void shutdown()

3. 代碼

  • 代碼: 在這裏插入圖片描述
  • 結果:
    在這裏插入圖片描述

三、Lambda

1. 函數式編程思想

函數式編程思想: 強調做什麼,而不是以某種形式做(面向對象強調通過對象來做)


  • 使用面向對象思想:在這裏插入圖片描述
  • 函數式思想: 外賣的核心需求是爲了創建中國對象嘛?不,我們是爲了將run()體內的代碼讓Thread()知曉。把怎麼做 轉向做什麼 的本質上,而不在乎過程與實質 ,我們只是爲了一個結果JDK1.8中加入了Lambda表達式
    在這裏插入圖片描述

2. Lambda標準格式

匿名內部類的好處是省去了實現類的定義 , 缺點是語法複雜


// Lambda的標準格式
(參數類型 參數名稱) -> {代碼體}

() : 接口中抽象方法的參數列表
-> : 傳遞的意思
{} : 重寫接口的參數方法 

2.1 無參的示例

在這裏插入圖片描述

2.2 有參無返回

在這裏插入圖片描述

2.3 有參有返回

標準形式: 在這裏插入圖片描述

3. Lambda極簡式

凡是可根據上下文推導的,都可以省略

  1. (參數列表) : 括號中參數列表的數據類型,可以省略不寫
  2. (參數列表) : 括號中的參數只有一個,類型和括號都可省略
  3. {代碼體} :如果代碼體只有一行,可以省略{}、return 、分號(這三個必須一起省略,要麼一起保留)

一些案例:


在這裏插入圖片描述
在這裏插入圖片描述


  • 使用Lambda表達式,必須要有一個接口,且該接口只有一個抽象方法
  • 必須具備上下文推斷

注: 只有一個抽象方法的接口叫函數式接口

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章