文章目錄
一、多線程安全
當多個線程之間,訪問了共享的數據,就會產生線程安全問題
1. Demo:賣票
我們以一個售票廳的例子舉例說明:
- 總共100張票,作爲共享數據
- 三個售票廳,一起售賣票,作爲三個線程
1.1 發生線程安全問題
結果:
會發現:
- 賣出了重複的票
- 賣出了不存在的票
1.2 線程安全問題原理
- 多個線程搶奪CPU的執行權
- 當執行到睡眠 時,線程把搶到的CPU執行權交出
- 當一個線程已經判斷完畢(無論如何,這部分函數體會執行),另一個線程改變了判斷條件也無濟於事,這時會出現條件外的值(不存在)
- 因爲多個線程共享資源,判斷依據相同,就會出現重複的情況
- 總結: 所以,安全問題 的實質就是共享數據 時,一個線程讀時,另一個線程進行寫的操作,這個寫操作,改變之前線程讀的結果
所以,如果數據共享 ,但線程中只進行讀 ,是不會存在安全問題的。
- 解決方法: 一個線程失去對
CPU
的控制權時,其它線程只能等待其執行完
2. 同步機制
同一時間,只能執行一件事,就叫同步
同步有三種方式:
- 同步代碼塊
- 同步方法
- 鎖機制
2.1 方法1:同步代碼塊
同步代碼塊 裏放可能會產生線程安全的代碼(訪問了共享數據的代碼),表示只對這個區塊的資源進行互斥訪問
synchronized(同步鎖){
// 需要同步操作的代碼
}
注意:
- 同步代碼塊中的對象,可以是任意對象
- 多個線程的鎖對象 必須是同一個
- 鎖對象作用: 把同步代碼塊鎖住,只讓一個線程在同步代碼塊中執行
其它部分代碼不變,使用代碼塊包住需要鎖的部分
2.2 同步技術原理
使用了一個鎖對象 ,這個鎖對象叫同步鎖 ,也叫對象鎖、對象監視器
t0
搶到了CPU
的執行權,執行run()
方法,遇到synchronized
代碼塊- 檢查該代碼塊是否有鎖對象
- 如果有,就會獲取該鎖對象,進入代碼塊中執行
t1
搶到了CPU
的執行權,執行run()
方法,遇到synchronized
代碼塊- 檢查該代碼塊是否有鎖對象,發現沒有
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
步驟:
- 在成員位置,創建一個
ReentrankLock
對象 - 在可能出現線程安全的地方,獲取鎖
- 在這段代碼結束時,釋放鎖
提示: 最好把代碼放在try...catch
中,並且把釋放鎖 放在finally
中,這樣無論有沒有異常,都會釋放鎖。
3. 線程狀態
Thread
中有一個嵌套類(內部類)Thread.State
,記錄着線程的狀態
3.1 無限等待狀態
下面,以線程通信案例 來進行說明
- 創建一個線程,等待另一個線程的執行結果,調用
wait();
方法,進入無限等待狀態 - 另一個線程調用
notify();
,退出無限等待狀態
注意事項:
- 兩個線程必須得用同步代碼塊包裹起來,以保證等待和喚醒,只有一個在執行
- 同步使用的鎖對象,必須保證唯一
- 只有鎖對象才能調用
wait()
和notify()
(這兩個都是Object類
中的方法)
兩個方法:
- wait() 在其它線程調用此對象的notify()和notifyall()方法前,將導致線程等待
- notify()喚醒在此對象監視器上等待的單個線程,會繼續執行wait方法之後的代碼
-
代碼:
-
結果:
注意: obj.wait()
如果不加參數,就是無限等待。但它可以接收一個毫秒值做參數,等到時間走完,還沒有得到喚醒,會自動醒來 ,此時相當於Thread.sleep()
方法
3.2 notifyAll
notify()
如果有多個等待線程,隨機喚醒一個
notifyAll()
如果有多個等待線程,全部喚醒
可以發現,一次只喚醒一個線程(隨機的)
把notify()
變爲notifyall()
會得到如下結果
4. 線程間通訊
線程間通訊: 多個線程在處理同一資源,但線程的任務不同
多個線程操作同一份數據,就會發生數據的搶奪,通過等待喚醒機制 可以避免這種爭奪發生
注意: 被通知(notify)後,只是進入了可運行 狀態,不是立即執行的(因爲還在同步代碼塊那,還要搶鎖)
4.1 Demo:服務器返回一個網頁
分析:
- 頁面數據: 請求,響應
- 服務器: 接收請求,發送響應
- 客戶端: 發送請求,接收響應
兩個線程的狀態是通信(互斥)狀態,必須保證兩個線程只有一個在執行
-
定義一個頁面準備的類,這個頁面需要文件,還需要資料渲染,還要請求響應狀態
-
定義一個服務器:當有請求時,如果有響應,返回響應。如果沒響應,就進行響應。如果沒請求,就休息等待請求。
-
定義一個客戶端:當無請求時,發送請求,當有請求時,請求響應。請求響應時,無限等待。
-
定義一個運行用的主文件(我是用同級類的寫法)
-
結果
注:
- 可以在服務器和客戶端類中,添加死循環
- 現實業務中,服務器做的是溝通連接的工作,處理頁面的工作交給應用框架
二、線程池
線程池: 容納多個線程的容器,其中的線程可以複用,無需反覆創建線程而消耗過多的資源
1. 線程池原理
線程池是一個容器 ,也即一個集合(LinkedList<Thread>
)
步驟:
- 程序開始的時候,創建多個線程,放進一個集合中
- 取出線程:
Thread t = List.remove(index)
、Thread t = LinkedList.removeFirst()
(線程只能被一個任務使用) - 放回線程:
List.add(t)
、LinkedList.addLast(t)
JDK1.5
後,JDK
內置了線程池
,可以直接使用。
線程池的好處:
- 降低資源消耗: 減少了創建和銷燬線程的次數,使得線程可以複用。
- 提高響應速度: 因爲線程在任務開始前就已經創建好了,所以在任務開始時,可以立即響應。
- 提高可管理性: 一個線程所佔內存約爲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極簡式
凡是可根據上下文推導的,都可以省略
- (參數列表) : 括號中參數列表的數據類型,可以省略不寫
- (參數列表) : 括號中的參數只有一個,類型和括號都可省略
- {代碼體} :如果代碼體只有一行,可以省略{}、return 、分號(這三個必須一起省略,要麼一起保留)
一些案例:
- 使用Lambda表達式,必須要有一個接口,且該接口只有一個抽象方法
- 必須具備上下文推斷
注: 只有一個抽象方法的接口叫函數式接口