Java 併發編程基礎#
《併發編程的藝術》閱讀筆記第四章,重點在於線程間的通信
一、線程簡介#
首先對線程做一個簡單的介紹
使用多線程的原因:
1.更多的處理器核心:一個線程在一個時刻只能運行在一個處理器核心上
2.更快的響應時間
3.更好的編程模型
JAVA 程序運行所需線程(jdk1.8)
線程優先級
操作系統基本採用時分的形式調度運行的線程,操作系統會分出一個個時間片,線程會分配到若干時間片,當線程的時間片用完了就會發生線程調度,並等待著下次分配,線程分配到的時間片多少就決定了線程使用處理器資源的多少。
有些操作系統會忽略對線程優先級的設定
在 Java 線程中,通過一個整型成員變量priority
來控制優先級,優先級的範圍從1~10
,在線程構建的時候可以通過setPriority(int)
方法來修改優先級,默認優先級是5
,優先級高的線程分配時間片的數量要多於優先級低的線程
線程的狀態
6
種,在給定的時刻只能處於一種狀態
NEW
:初始狀態,線程被構建,但還沒有調用 start 方法RUNNABLE
:運行狀態,java 線程將 == 就緒和運行 == 兩種狀態統稱為運行狀態BLOCKED
:阻塞狀態,表明線程阻塞於鎖WAITING
:等待狀態,等待其他線程的通知或中斷TIME_WAITING
:超時等待狀態,可以在指定的時間自行返回TERMINATED
:終止狀態,表示當前線程已經執行完畢
線程狀態變遷
Java 將操作系統中的運行和就緒兩個狀態合併稱為運行狀態。
阻塞狀態是線程阻塞在進入
synchronized
關鍵字修飾的方法或代碼塊(獲取鎖)時的狀態但是阻塞在
java.concurrent
包中 Lock 接口的線程狀態卻是等待狀態,因為java.concurrent
包中 Lock 接口對於阻塞的實現均使用了LockSupport
類中的相關方法
Daemon 線程(守護線程)
指在程序運行的時候在後台提供一種通用服務的線程,比如垃圾回收線程。不屬於程序中不可或缺的部分。
當一個 java 虛擬機中不存在非守護線程時,虛擬機將會退出。可以通過設置Thread.setDaemon(true)
將線程設置為 daemon 線程,必須在線程啟動前設置
ps. 在構建 Daemon 線程時,不能依靠 finally 塊中的內容來確保執行關閉或清理資源的邏輯,可能不執行
二、啟動和終止線程#
一個新構造的線程對象是 == 由其父線程 == 來進行空間分配的,而子線程的各種屬性 == 繼承自父線程,同時還會分配一個唯一的 ID。==
看Thread.init()源碼
jdk1.8的源碼中添加了很多關於ThreadGroup的判斷操作
估計是對線程的安全性和異常情況做了更完善的區分
** 啟動線程:**start 方法:當前線程同步告知虛擬機:只要線程規劃器空閒,應立即啟動調用 start 方法的線程,== 最好在線程創建之前設置線程名稱,有利於排查錯誤 ==
中斷:
可以理解為線程的一個標識符屬性,它標識一個運行中的線程是否被其他線程進行了中斷操作。是一種簡便的線程間交互方式。
線程通過isInterrupted()
方法判斷是否被中斷,也可以調用靜態方法Thread.interrupted()
對當前的線程中斷標識位進行復位。
從java的API可以看出,許多聲明拋出InterruptedException的方法在拋出這個異常前,java虛擬機會先將該線程的中斷標誌位清除,然後再拋異常。此時調用isInterrupted返回false
- 當線程處於等待狀態或者有超時的等待狀態時(
TIMED_WAITING
,WAITING
)我們可以通過調用線程的interrupt()
方法來中斷線程的等待,此時線程會拋InterruptedException
異常。 - 但是當線程處於
BLOCKED
狀態或者RUNNABLE
(RUNNING
)狀態時,調用線程的interrupt()
方法也只能將線程的狀態位設置為true
。停止線程的邏輯需要我們自己去實現。
過期 API:suspend()
,resume()
,stop()
原因:佔有資源的時候容易發生死鎖問題,讓線程共工作在不確定狀態下
終止線程的做法: 一種方法可以使用interrupt()
方法,或者利用一個 boolean 變量將線程終止,使線程在終止時有機會去清理資源
三、線程間通信(重點)#
1、volatile 和 synchronized 關鍵字#
volatile:告知程序任何對該變量的訪問均需要從共享內存中獲取,而對他的改變必須同步刷新回主內存,他能保證所有線程對變量訪問的可見性。
synchronized:確保多個線程在同一個時刻,只能有一個線程處於方法或同步塊中,保證了線程對變量訪問的可見性和排他性。不能保證重排序
關於synchronized
:本質是對一個對象的監視器(monitor
)的獲取,而這個獲取過程是排他的,也就是同一時刻只能有一個線程獲取到有synchronized
所保護對象的監視器。任意一個對象都擁有自己的監視器。
任意線程對 Object 的訪問,首先要獲得 Object 的監視器。如果獲取失敗,線程進入同步隊列,線程狀態變為
BLOCKED
。當訪問 Object 的前驅(獲得了鎖的線程)釋放了鎖,則該釋放操作喚醒阻塞在同步隊列中的線程,使其重新嘗試對監視器的獲取。
2、等待 / 通知機制(生產者 - 消費者模型)#
等待 / 通知機制,是指一個線程 A 調用了對象 O 的
wait
方法進入等待狀態,而另一個線程 B 調用了對象 O 的notify
或notifyall
方法,線程 A 收到通知後從對象 O 的wait
方法返回,進而執行後續操作。
此處的代碼示例失效,wait線程沒有正常被喚醒,也沒有發生重排序
原因是因為獲取的鎖不是同一個鎖,因此導致無法正常解鎖和加鎖,需要把同步的對象設置為public static確保獲得對象是同一個
///示例不是這個問題,是因為一開始沒有wait...、😂
注意:
(1)使用wait
、notify
、notifyAll
時需要先對調用對象加鎖
(2)調用 wait 方法後,線程狀態由RUNNING
變為WAITING
,並將當前線程防止到對象的等待隊列
(3)notify
或notifyAll
方法調用後,等待線程依舊不會從wait
返回,需要調用notify
或notifyAll
的線程釋放鎖之後,等待線程才有機會從wait
返回
(4)notify
方法將等待隊列中的等待線程從等待隊列中移到同步隊列中,被移動的線程從WAITING
變為BLOCKED
(5)從wait
方法返回的前提是獲得了調用對象的鎖
3、等待 / 通知經典範式#
等待方:1.獲取對象的鎖
2.如果條件不滿足,那麼調用對象的wait方法,被通知後仍要檢查條件
3.條件滿足則執行對應的邏輯
偽代碼:
synchronized(對象){
while(條件不滿足){
對象.wait();
}
對應的處理邏輯
}
通知方:1.獲得對象的鎖
2.改變條件
3.通知所有等待在對象上的線程
偽代碼:
synchronized(對象){
改變條件
對象.notify();
}
4、管道輸入輸出流#
管道輸入 / 輸出流和 普通的文件輸入 / 輸出流 或者 網絡輸入 / 輸出流 不同之處在於,它主要用於線程之間的數據傳輸,而傳輸的媒介為內存
PipedOutputStream
、PipedInputStream
(面向字節數據)
PipedReader
、PipedWriter
(面向字符)
對於piped
類型的流,使用時必須先調用connect
方法進行綁定,否則會拋出異常
管道輸入緩衝區大小默認 1024 字節
寫示例代碼的注意事項:
1. write寫入 read讀出
2. 打印的時候用的是System.out.print 而不是 System.out.prineln.後者會最後加回車
5、Thread.join()#
含義:當前線程等待 thread 線程終止之後才從thread.join
返回。
另外還有兩個join(long millis)
和join(long millis,int nanos)
具備 == 超時特性 == 的方法(如果線程在給定的時間內沒有終止,那麼將會從該超時方法返回)。
join()等價於wait(0),只要等指定線程鎖釋放就可以搶佔鎖
如果指定時間後,就需要指定時間之後才能重新擁有鎖(wait加參數本質上還是定時調用notify()方法,只是這段代碼在JVM裡面,比較底層)
join如果加參數,第一次代表按照參數設置wait喚醒時間,之後循環檢測線程活性,如果線程始終存活,才開始傳入設置值和當前時間的差值
6、ThreadLocal#
ThreadLocal,即線程變量,是一個以 ThreadLocal 對象為鍵、任意對象為值的存儲結構。這個結構被附帶在線程上,也就是說一個線程可以根據一個 ThreadLocal 對象查詢到綁定在這個線程上的一個值。
是一個線程安全的局部變量
四、應用實例#
一、等待超時模式#
調用一個方法時等待一段時間(一般來說是給定一個時間段),如果該方法能夠在給定的時間段之內得到結果,那麼將結果立刻返回,反之,超時返回默認結果。
public synchronized Object get(long mills) throws InterruptedException {
long future = System.currentTimeMillis() + mills;
long remaining = mills;
// 當超時大於0並且result返回值不滿足要求
while ((result == null) && remaining > 0) {
wait(remaining);
remaining = future - System.currentTimeMillis();
}
return result;
}
典型案例:數據庫連接池模式
源碼略
CountDownLatch:
- countDownLatch 這個類使一個線程等待其他線程各自執行完畢後再執行。
- 是通過一個計數器來實現的,計數器的初始值是線程的數量。每當一個線程執行完畢後,計數器的值就 - 1,當計數器的值為 0 時,表示所有線程都執行完畢,然後在閉鎖上等待的線程就可以恢復工作了。
二、線程池技術#
客戶端可以通過 execute (Job) 方法將 Job 提交入線程池執行,而客戶端自身不用等待 Job 的執行完成。除了 execute (Job) 方法以外,線程池接口提供了增大 / 減少工作者線程以及關閉線程池的方法。
這裡工作者線程代表著一個重複執行 Job 的線程,而每個由客戶端提交的 Job 都將進入到一個工作隊列中等待工作者線程的處理。
線程池的本質就是使用了一個線程安全的工作隊列連接工作者線程和客戶端線程,客戶端線程將任務放入工作隊列後便返回,而工作者線程則不斷地從工作隊列上取出工作並執行
典型案例:實現一個 web 服務器
源碼略