目錄
1. 線程介紹
概念
- 程序:靜態的概念(資源分配的單位)
- 進程:運行的程序(調度執行的單位)
- 線程:一個程序中有多個事件同時執行,每個事件一個線程,多個線程共享代碼和數據空間
2. 創建並啓動線程
當JVM啓動時,會創建一個非守護線程 main
,作爲整個程序的入口,以及多個與系統相關的守護線程
。
package concurrency.chapter1;
public class TryConcurrency {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
write();
}
}.start();
new Thread(){
@Override
public void run() {
read();
}
}.start();
}
private static void write(){
System.out.println("start to write");
try {
Thread.sleep(1000*10L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("write success!");
}
private static void read(){
System.out.println("start to read");
try {
Thread.sleep(1000*10L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("read success!");
}
}
線程生命週期(Thread.State)
除了新生狀態和死亡狀態,任意狀態都可能發生線程死亡。
- NEW:新生狀態,作爲普通java對象,尚未啓動
- RUNNABLE:在JVM中執行(包含就緒狀態和運行狀態)
- BLOCKED:被阻塞
- WAITING:正在等待另一個線程執行特定動作
- TIMED_WAITING:正在等待另一個線程達到指定運行時間
- TERMINATED:死亡狀態
傳統的多線程共享數據方法是使用static修飾,但是由於static修飾的數據將在整個程序結束之後才釋放,因此比較耗資源。可參考內存模型,類加載
而且這種方式會發生併發問題。
實例:銀行有多個櫃檯,櫃檯共用一個叫號系統
package concurrency.chapter2;
/**
* 叫號的櫃檯
*/
public class TicketWindow extends Thread{
private static final int MAX=500;
private static int index=1;
private String name;
TicketWindow(String name){
this.name=name;
}
@Override
public void run() {
while (index<=MAX){
System.out.println(name+" 叫號:"+(index++));
}
}
}
package concurrency.chapter2;
/**
* 銀行叫號
*/
public class Bank {
public static void main(String[] args) {
final TicketWindow t1 = new TicketWindow("一號櫃檯");
final TicketWindow t2 = new TicketWindow("二號櫃檯");
final TicketWindow t3 = new TicketWindow("三號櫃檯");
t1.start();
t2.start();
t3.start();
}
}
上述方式通過繼承Thread
的形式實現多線程,但是顯然,業務數據與被混在了線程類當中,這種方式顯得混亂,我們使用更好的辦法——使用runnable。
package concurrency.chapter2;
public class Bank2 {
public static void main(String[] args) {
final TicketWindow2 ticketWindow2 = new TicketWindow2();
Thread t1 = new Thread(ticketWindow2,"1號櫃檯");
Thread t2 = new Thread(ticketWindow2,"2號櫃檯");
Thread t3 = new Thread(ticketWindow2,"3號櫃檯");
t1.start();
t2.start();
t3.start();
}
}
package concurrency.chapter2;
/**
* 叫號的櫃檯
*/
public class TicketWindow2 implements Runnable{
private static final int MAX=500;
private static int index=1;
public void run() {
while (index<=MAX){
System.out.println(Thread.currentThread().getName()+" 叫號:"+(index++));
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
依舊有線程安全問題。
3. 函數式接口編程
Thread支持函數式接口編程,Runnable接口就是一個函數式接口。
首先看看什麼是函數式接口編程。
Java中使用@FunctionalInterface註解標註函數式接口。
所謂的函數式接口,當然首先是一個接口,然後就是在這個接口裏面只能有一個抽象方法
。
這種類型的接口也稱爲SAM接口,即Single Abstract Method interfaces
特點
- 接口有且僅有一個抽象方法
- 允許定義靜態方法
- 允許定義默認方法
- 允許java.lang.Object中的public方法
@FunctionalInterface註解不是必須的,如果一個接口符合"函數式接口"定義,那麼加不加該註解都沒有影響。加上該註解能夠更好地讓編譯器進行檢查。如果編寫的不是函數式接口,但是加上了@FunctionInterface,那麼編譯器會報錯
例子
// 正確的函數式接口
@FunctionalInterface
public interface TestInterface {
// 抽象方法
public void sub();
// java.lang.Object中的public方法
public boolean equals(Object var1);
// 默認方法
public default void defaultMethod(){
}
// 靜態方法
public static void staticMethod(){
}
}
// 錯誤的函數式接口(有多個抽象方法)
@FunctionalInterface
public interface TestInterface2 {
void add();
void sub();
}
函數式接口其實策略模式中的“策略”
計算納稅款的實例:
納稅款的計算方式可能多變,因此將計算方式抽象出來作爲一個接口(策略),然後可以實現動態的改變改計算方法。
接口:傳入工資
package concurrency.func;
@FunctionalInterface
public interface SimpleCalculator {
double calculated(double money);
}
計算器:關聯上述接口
package concurrency.func;
public class Calculator {
private double money;
private final SimpleCalculator calculator;
Calculator(double money,SimpleCalculator calculator){
this.calculator=calculator;
this.money=money;
}
public double calculated(){
return calculator.calculated(money);
}
}
客戶端
public class CalculatorMain {
public static void main(String[] args) {
final Calculator calculator = new Calculator(10000, (m)->m*0.2 );
System.out.println(calculator.calculated());
}
}
通過傳入不同的lambda表達式,實現不同的計算方法。
通過函數式接口編程,將銀行叫號整合到一個類中。
public class Bank3 {
public final static int MAX=50;
public static int index=1;
public static void main(String[] args) {
final Runnable runnable = ()->{
while (index<=50){
System.out.println(Thread.currentThread().getName()+" 櫃檯叫號 "+(index++));
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
new Thread(runnable,"1號櫃檯").start();
new Thread(runnable,"2號櫃檯").start();
new Thread(runnable,"3號櫃檯").start();
}
}
上述代碼同樣有嚴重的併發問題。
4. Thread 構造器
構造器
1.Thread()
只做命名工作,命名方式爲
Thread-i (i從0開始)
2.Thread(Runnable)
因爲runnable只有一個run方法,因此相當於聲明一個run方法。如果Runnable爲空,則線程什麼也不做。
3.Thread(String)
傳入線程名字,由於Runnable爲空,因此什麼也不做。
4.Thread(Runnable,String)
聲明run方法,以及線程名字,此時就是一個可工作的線程。
5.ThreadGroup的概念:
在構造器中可以傳入一個ThreadGroup,當ThreadGroup爲空時,查看源碼後可知會設定CurrentThread的ThreadGroup爲當前的ThreadGroup。
CurrentThread就是啓動當前線程的線程。
比如在main函數中啓動的線程,那麼CurrentThread就是main,main的ThreadGroup名爲main
6.stacksize概念
首先得知道基本的內存模型
堆和方法區是線程共享,而虛擬機棧是線程私有,stacksize主要影響虛擬機棧。
棧作爲內存結構,肯定有限制大小,通過改變stacksize,能夠更改自定義線程的虛擬機棧大小。
在官方文檔中有說明,stacksize高度依賴平臺,不同的運行平臺,stacksize可能不起作用。
如果不傳stacksize,則默認爲0,表示會被忽略。
stacksize被JVM使用,Java中沒有直接引用。
5. 守護線程
Daemon:守護線程,會跟隨父線程結束。
非守護線程(默認
),父線程結束之後,依舊運行。
這麼理解: “守護”的對象指的是系統,而非線程本身。當父線程結束之後子線程依舊還在跑,可能出現一些意外的情況,因此需要對系統進行守護。
問題:
1.非守護線程outer中有個長耗時的守護線程inner,那麼當outer結束時,inner是什麼狀態?
inner是守護線程,會隨父線程結束,因此當outer結束後,inner未執行完就結束了。
2.守護線程outer中有個長耗時的非守護線程inner,那麼當outer結束時,inner是什麼狀態?
inner是非守護線程,不會隨父線程結束,因此當outer結束後它會繼續運行。
無論外層的線程是什麼類型,只關注本身的類型即可。
通過T.setDaemon(true)設爲守護線程。
注意,只有在start之前設置才生效。
線程關係
以下內容爲個人實驗結果,可能存在偏頗之處
線程之間的關係分爲三種:
最外層:線程之間是平等的,線程開始之後,就有了自己獨立的運行空間,不會受執行該線程的線程所影響
舉例:守護線程中執行非守護線程,當守護線程隨着main結束之後,內部的非守護線程依舊在執行,既不會阻塞守護線程,也不會跟着守護線程結束。
package concurrency.chapter1;
public class Try {
public static void main(String[] args) throws InterruptedException {
// 守護線程中套一個長時間的非守護線程,
// 一般來說,當main結束後,守護線程會結束,但是由於其內還有個非守護線程,那麼它會被阻塞住嗎?
Thread t1 = new Thread(()->{
Thread t2 = new Thread(()->{
while (true){
System.out.println("儘管父線程都結束了,但是我還在跑");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.setDaemon(false);
t2.start();
System.out.println("t1結束");
});
t1.setDaemon(true);
t1.start();
// 模擬t1在工作,
// 如果不加主線程睡眠,那麼程序會直接結束
// 猜測:t2的創建需要耗時間,還沒等t2創建,main就結束了,也導致守護線程t1結束
Thread.sleep(100);
}
}
關係二:線程之間存在執行與被執行關係
舉例:CurrentThread、Thread的靜態方法受執行位置所影響、join等都是這個關係的體現。
比較重要的應用就是內部爲守護線程時,內部線程的運行時間受外部線程所影響。
關係三:ThreadGroup
這個通常是人爲設置,將多個線程放在一個組中進行管理。
6. join
讓CurrentThread等待join線程執行完畢,CurrentThread才繼續前進。
在main方法中使用Thread.CurrentThread.join(),相當於讓main等待自己結束,這會陷入死循環。
7. interrupt
當線程處於block狀態(wait、join、sleep)時,調用interrupt會讓線程接收一個InterruptException(如果線程中沒有捕獲該異常,即便interrupt了,線程也不會收到中斷信號)
interrupt不會中斷線程,而是將線程的狀態改爲中斷。通過在線程內捕獲該狀態,然後邊寫處理代碼,實現中斷。
block狀態也有對象之分:x.wait、x.sleep,當x是誰(或者在哪個線程內)調用,則wait、sleep對象就是x(或當前調用的線程),但是x.join,對象僅僅指CurrentThread,如在main中調用thread1.join(),join的對象是main,即main進入join狀態,而非thread1。
根據上述描述,interrupt可以打斷block狀態的線程,當線程處於join時,可以進行打斷。
但是注意進入join狀態的線程是哪個。
可以試試以下實驗:
package concurrency.chapter3;
public class Interrupt {
public static void main(String[] args) {
// 啓動測試線程對象 t
Thread t = new Thread(()->{
while (true){
}
});
t.start();
// 由於t.join()之後的代碼都不執行,因此新建一個臨時線程用於監控t.join()之後的狀態
new Thread(()->{
try {
// sleep保證t.join()執行完畢
Thread.sleep(1000);
// 拿到main線程
ThreadGroup group = Thread.currentThread().getThreadGroup();
Thread[] threads = new Thread[group.activeCount()];
group.enumerate(threads);
for (Thread thread:threads){
if (thread.getName().equals("main")){
System.out.println("main state:"+thread.getState());
System.out.println("t state:"+ t.getState());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
try {
// 在main中調用t.join()
System.out.println("t.join() invoked in main()");
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
結果爲
t.join() invoked in main()
main state:WAITING
t state:RUNNABLE
說明t.join()之後,main進入join阻塞狀態,而非t。
因此,我們通過以下代碼想實現通過join打斷t是不行的,因此進入join的不是t,而是CurrentThread
package concurrency.chapter3;
public class Interrupt {
public static void main(String[] args) {
// 啓動測試線程對象 t
Thread t = new Thread(()->{
while (true){
}
});
t.start();
// 由於t.join()之後的代碼都不執行,因此新建一個臨時線程用於監控t.join()之後的狀態
new Thread(()->{
System.out.println("在臨時線程中interrupt->t");
t.interrupt();
}).start();
try {
// 在main中調用t.join()
System.out.println("t.join() invoked in main()");
t.join();
} catch (InterruptedException e) {
System.out.println("t在join過程中被interrupt");
}
}
}
以上代碼相當於改變了t的interrupt狀態爲中斷狀態,但是沒有對象去捕獲該異常進行處理。
因此,通過join捕獲interrupt異常,只能捕獲CurrentThread。
8. 優雅的結束線程
方式一:flag
package concurrency.chapter4;
public class Stop1 {
public static class Test extends Thread{
private volatile boolean start=true;
@Override
public void run() {
while (start){
}
}
public void shutdown(){
this.start=false;
}
}
public static void main(String[] args) {
Test test = new Test();
test.start();
// 模擬test工作耗時
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.shutdown();
}
}
方式二:使用interrupt+block
package concurrency.chapter4;
public class Stop2 {
public static void main(String[] args) {
Thread thread = new Thread(()->{
while (true) {
try {
Thread.sleep(0);
} catch (InterruptedException e) {
System.out.println("結束進程");
return;
}
}
});
thread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
}
方式三:Thread.interrupted()
Thread.interrupted()用於檢測當前線程是否被interrupt,相當於thread.isInterrupted(),只是一個是靜態方法,一個是實例方法。
之所以多一個靜態方法,是因爲在lambda或者匿名內部類中不能使用實例方法。
package concurrency.chapter4;
public class Stop2 {
public static void main(String[] args) {
Thread thread = new Thread(()->{
while (true) {
if (Thread.interrupted()){
System.out.println("結束進程");
return;
}
}
});
thread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
}
考慮以下情況,在線程中,有個事件本身就block了(比如預計10秒,但是一個小時也沒結束),這時候,它無法讀取到flag,也監聽不到interrupted,該如何結束它?
利用守護線程與非守護線程的概念。
父線程結束,守護線程也會結束,因此,我們可以用一個父線程來執行目標線程,並將目標線程設爲守護線程。通過控制父線程的結束,實現目標線程的結束。
方式四:利用守護線程
**父線程ThreadService **
package concurrency.chapter4;
public class ThreadService {
private Thread executeThread;
private volatile boolean finished=false;
private String taskName;
ThreadService(String taskName){
this.taskName=taskName;
}
// 將執行任務設爲守護線程
public void execute(Runnable task){
executeThread = new Thread(()->{
Thread runner = new Thread(task);
runner.setDaemon(true);
runner.start();
// executeThread等待runner執行完畢
try {
runner.join();
finished=true;
} catch (InterruptedException e) {
// 打斷,結束程序體
}
});
executeThread.start();
}
public void shutdown(long mills){
long currentTime = System.currentTimeMillis();
// 如果沒有結束
while (!finished){
// 如果超時
if (System.currentTimeMillis()-currentTime>mills){
// 並非interrupt()結束了線程,而是interrupt()控制了線程執行語句
// 調用本行代碼時,會進入第21行,
// 由於executeThread本身沒有其他處理邏輯,因此executeThread結束,同時runner也跟着結束
executeThread.interrupt();
System.out.println(this.taskName+" 任務超時!強行結束");
break;
}
// 由於finished加了volatile關鍵字,因此當線程結束時,此處的finished也會被觀測爲true
// 所以如果沒有超時,while也能夠自動斷開
}
finished=false;
}
}
客戶端實例
package concurrency.chapter4;
public class Stop3 {
public static void main(String[] args) {
ThreadService threadService = new ThreadService("test線程");
long start = System.currentTimeMillis();
threadService.execute(()->{
// 執行一個非常重的任務
// while (true){
// }
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
threadService.shutdown(1000);
long end = System.currentTimeMillis();
System.out.println(end-start);
}
}
9. 線程安全、數據共享
同步鎖 synchronized
可鎖對象:
- 實例對象:this,synchronized作爲實例方法關鍵字時,
- 類對象:class,synchronized作用於靜態方法、修飾靜態對象時
- 塊對象:鎖住對應字節碼
死鎖:相互等待對方的資源,一般發生在鎖套鎖的情況。
synchronized核心
實例鎖:鎖特定實例,this
類鎖:鎖類,class
每次執行之前模擬以下過程:判斷是否需要鎖-->鎖競爭-->得到鎖-->執行-->釋放鎖
①synchronized修飾非靜態方法-->鎖this
-->1.1 多個線程訪問同一個對象的該方法-->同步
-->1.2 同一個對象,一個線程訪問synchronized方法,另一個對象訪問非synchronized方法-->異步
-->1.3 同一個對象,一個線程訪問synchronized方法,另一個對象訪問另一個synchronized方法-->同步
結論:一個實例只有一個this鎖,
對於1.1,存在鎖競爭的過程,因此同步
對於1.2,非synchronized方法不需要鎖競爭,因此異步
對於1.3,儘管是兩個不同的synchronized方法,但是是同一個鎖,也需要鎖競爭,因此同步
②synchronized修飾靜態方法-->鎖class
-->2.1 一個線程訪問synchronized靜態方法,另一個對象訪問非synchronized靜態方法-->異步
-->2.2 一個線程訪問synchronized靜態方法,另一個對象訪問另一個synchronized靜態方法-->同步
對於2.1,非synchronized方法不需要鎖競爭,因此異步
對於2.2,儘管是兩個不同的synchronized方法,但是是同一個鎖,也需要鎖競爭,因此同步
③定義靜態變量lock,通過synchronized(lock){}對代碼塊進行加鎖-->鎖lock變量
-->3.1 一個線程訪問synchronized靜態方法,另一個線程訪問包含synchronized(lock){}的靜態方法-->異步
-->3.2 一個線程訪問synchronized靜態方法,另一個線程訪問包含synchronized(當前類名.class){}的靜態方法-->同步
對於3.1,一個鎖是class,一個鎖是lock,不存在鎖競爭,因此異步
對於3.2,兩個鎖都是當前類名.class,存在鎖競爭,因此異步
注意,有種lock的寫法是 private static 當前類名 lock = new 當前類名();
此時的lock與當前類名.class依舊不是同一個鎖。
④定義靜態變量int i,一個線程運行synchronized方法修改i,另一個線程運行費synchronized方法修改i,此時是異步,而且數據不安全。
--> synchronized沒有鎖住資源,只鎖住了代碼,在其他入口訪問同一份資源依舊會出現數據不同步問題。
⑤定義靜態代碼塊內的synchronized
static{
synchronized(xx){}
}
-->靜態代碼塊會阻塞該類中的所有資源,因爲加載靜態代碼塊屬於類的初始化過程
綜上,其實synchronized的核心就在於加鎖過程,我們需要判斷當前鎖是否存在、是否是同一個鎖對象,進而判斷是否存在鎖競爭,從而得知是否是同步、異步。
上述內容皆爲實驗所得,如有遺漏,敬請留言。
10. 死鎖
死鎖:相互等待對方的資源,一般發生在鎖套鎖的情況。
A等待B,B等待C,C等待A
常見於以下情況:
在使用第三方service時,第三方service需要傳入我們自己寫的類。我們自己寫的類又加了鎖,就可能出現鎖套鎖的情況。
檢測死鎖的方法:
打開cmd
jps 查看所有java進程
jstack 進程號 如果有死鎖,控制檯會通知
11. 線程間的通訊(生產者與消費者)
模型一:單生產者+單消費者
package concurrency.chapter6;
// 生產者消費者模型
public class ProductorConsumerModel {
private int i;
private boolean needProduct=true;
void product(){
synchronized (this) {
// 如果需要生產,那麼就生產
if (needProduct) {
System.out.println("生產:" + (i++));
// 生產完了通知消費者消費
this.notify();
needProduct=false;
}else{
// 如果不需要生產,那麼就讓生產者等待。
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
void comsumed(){
synchronized (this) {
// 如果不需要生產,那就消費
if (!needProduct) {
System.out.println("消費:" + (--i));
// 消費完了通知生產者生產
this.notify();
needProduct=true;
}else {
// 需要生產,就讓消費者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
ProductorConsumerModel model = new ProductorConsumerModel();
new Thread(()->{
while (true){
model.product();
}
}).start();
new Thread(()->{
while (true) {
model.comsumed();
}
}).start();
}
}
注意,xx.wait()是讓執行這個方法的線程等待,直到被通知。xx.notify()是喚醒被當前鎖鎖住的對象
這個模型只允許構建一個生產者線程和一個消費者線程,多個生產者消費者的時候會出現假死鎖狀態。
因爲不能確定notify作用於哪個對象,導致所有線程進入wait狀態
。
模型二:多消費者-多生產者
把模型一中的notify改爲notifyAll()即可(注意被喚醒並且被執行的線程是從上次阻塞的位置從下開始運行,也就是從wait()方法後開始執行。
因此判斷是否進入某一線程的條件 是用while判斷,而不是用If判斷判斷。)
package concurrency.chapter6;
// 生產者消費者模型
public class ProductorConsumerModel2 {
private int i;
private boolean needProduct=true;
void product(){
synchronized (this) {
// 如果需要生產,那麼就生產
while (needProduct) {
System.out.println(Thread.currentThread().getName() + "生產:" + (i++));
// 生產完了通知消費者消費
this.notifyAll();
needProduct = false;
}
// 如果不需要生產,那麼就讓生產者等待。
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
void comsumed(){
synchronized (this) {
// 如果不需要生產,那就消費
while (!needProduct) {
System.out.println(Thread.currentThread().getName()+"消費:" + (--i));
// 消費完了通知生產者生產
this.notifyAll();
needProduct=true;
}
// 需要生產,就讓消費者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ProductorConsumerModel2 model = new ProductorConsumerModel2();
new Thread(()->{
while (true){
model.product();
}
},"P1").start();
new Thread(()->{
while (true){
model.product();
}
},"P2").start();
new Thread(()->{
while (true){
model.product();
}
},"P3").start();
new Thread(()->{
while (true){
model.product();
}
},"P4").start();
new Thread(()->{
while (true) {
model.comsumed();
}
},"C1").start();
new Thread(()->{
while (true) {
model.comsumed();
}
},"C2").start();
}
}
方式三:將數據、生產者、消費者解耦(重點掌握)
package concurrency.chapter6;
import concurrency.chapter4.ThreadService;
import java.util.stream.Stream;
// 生產者消費者模型
public class ProductorConsumerModel4 {
public static void main(String[] args) {
Data data = new Data();
Stream.of("P1","P2").forEach(name->
new Thread(()->{
Productor productor = new Productor(data);
while (true){
productor.producted();
}
},name).start());
Stream.of("C1","C2").forEach(name->
new Thread(()->{
Consumer consumer = new Consumer(data);
while (true){
consumer.consumed();
}
},name).start());
}
}
// 數據容器
class Data{
private int i=0;
private boolean needPush;
synchronized public void push() {
while (needPush){
System.out.println(Thread.currentThread().getName()+"生產: "+ (++i));
needPush=!needPush;
notifyAll();
}
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized public void pop() {
while (!needPush){
System.out.println(Thread.currentThread().getName()+"消費: "+ (i--));
needPush=!needPush;
notifyAll();
}
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 生產者
class Productor{
private Data data;
Productor(Data data){
this.data=data;
}
public void producted(){
data.push();
}
}
// 消費者
class Consumer{
private Data data;
Consumer(Data data){
this.data=data;
}
public void consumed(){
data.pop();
}
說明:原理其實非常簡單:生產者和消費者的速度都是一樣的,那麼理論情況下,生產一個,就被消費一個。因此,需要同步的對象就是當前這個被生產/被消費的數據,與之相關的操作就是“生產”與“消費”,
因此只需要讓這兩個操作去競爭同一個鎖就行了
。
思考:如果是一個生產者和一個消費者,並且生產者和消費者都在搶一個鎖(對數據的寫入和寫出權),這不還是單線程麼?
此外,如果隊列入口一個鎖,出口一個鎖,生產者和消費者各搶各的鎖,用容器上限來判斷是否生產\消費,那麼多個生產者和消費者的速度是不是還是跟單個生產者\消費者一樣?
畢竟隊列只有一個入口和出口,同一時刻只允許一個生產者和消費者操作。
上述問題博主暫時也沒找到答案,等以後學會了回來補充,如果各位看官懂,敬請留言指點
12. sleep與wait的區別
1.sleep是Thread方法,wait是Object方法
2.sleep不會釋放鎖,wait會釋放鎖
3.sleep不依賴於鎖,wait的使用依賴於鎖
4.sleep不需要喚醒,wait需要(wait(time)除外)
13. 綜合案例–數據採集
線程切換是需要開銷的,多線程效率是一個開口向下的拋物線,當線程過多的時候,效率會越來越慢。
案例:對n臺機器進行數據採集工作,顯然,我們需要定義一定數量的線程,當某個線程結束後,再啓動一個線程去採集,保證線程的數量不超過設定的最大值。
假設有10臺機器,線程最大數爲5。
package concurrency.chapter7;
import java.util.*;
import java.util.stream.Stream;
public class DataCapture {
final private static int MAXSIZE=5;
// FIFO隊列,用於控制運行時的線程個數
final static private LinkedList<Object> CONTROLS = new LinkedList<>();
public static void main(String[] args) {
// 由於流是一次性的,我們用一個容器臨時保存線程,保證在後面能夠讓所有線程join
List<Thread> threads = new ArrayList<>();
// 創建10個線程,注意我們能夠同時運行的線程最大個數爲5,因此通過wait對線程的運行進行控制
Stream.of("M1","M2","M3","M4","M5","M6","M7","M8","M9","M10")
.map(DataCapture::threadCreate)
.forEach(t->{
t.start();
threads.add(t);
});
// 拿到所有線程,進行join
threads.forEach(t->{
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Optional.of("所有線程工作結束").ifPresent(System.out::println);
}
private static Thread threadCreate(String name){
return new Thread(()->{
// 運行時控制線程個數,進隊列搶鎖
synchronized (CONTROLS) {
// 如果當前個數大於MAXSIZE了(也就是第六個)就讓其wait
while (CONTROLS.size() >= MAXSIZE) {
try {
System.out.println(Thread.currentThread().getName() + ": 正在等待!");
CONTROLS.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
CONTROLS.addLast(new Object());
// 這句通知由隊列發出(即在synchronized中),能夠清楚看到工作順序
System.out.println(Thread.currentThread().getName()+": 開始工作");
}
// 工作時都在自己的工作空間,不需要進行synchronized
// 模擬工作耗時
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": 工作結束");
// 出隊列搶鎖
synchronized (CONTROLS){
// 工作完畢,自己退出隊列,並通知其他線程
CONTROLS.removeFirst();
CONTROLS.notifyAll();
}
},name);
}
}
在個數判斷中必須使用while (CONTROLS.size() >= MAXSIZE) {
,大於等於號,才能保證同時運行5個線程,我也不知道爲啥。按照邏輯來說應該是用大於號,望懂得朋友留言告知。
14. 顯式鎖(實現自定義鎖)
API接口設計
package concurrency.chapter8;
import java.util.Collection;
// 顯式鎖API接口
public interface Lock {
// 超時異常
class TimeOutException extends Exception{
TimeOutException(String msg){
super(msg);
}
}
// 加鎖
void lock() throws InterruptedException;
// 按時加鎖
void lock(long mills) throws InterruptedException,TimeOutException;
// 解鎖
void unlock();
// 查看阻塞線程
Collection<Thread> getBlockedThreads();
// 查看阻塞個數
int getBlockedSize();
}
實現
package concurrency.chapter8;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
// 通過boolean值去操控鎖
package concurrency.chapter8;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.TimeoutException;
// 通過boolean值去操控鎖
public class BooleanLock implements Lock {
// 如果LOCK爲true,說明鎖被人持有,否則說明鎖爲空閒狀態
private boolean LOCK;
// 保證當前操作鎖的對象式currentThread,不然其他線程也能自由調用lock和unlock
private Thread currentThread;
// 保存當前被阻塞的線程
private Collection<Thread> blockedThreads = new ArrayList<>();
// synchronized不能在接口中聲明,在這裏進行標註
@Override
synchronized public void lock() throws InterruptedException {
// 如果鎖被人持有,那就等待
while (LOCK){
this.wait();
this.blockedThreads.add(Thread.currentThread());
}
// 結束while之後說明鎖被當前線程拿到,那就更改鎖狀態
this.blockedThreads.remove(Thread.currentThread());
LOCK=true;
this.currentThread = Thread.currentThread();
}
// synchronized加的鎖不具備超時的能力,因此我們自定義超時鎖
@Override
synchronized public void lock(long mills) throws InterruptedException, TimeOutException {
if (mills<=0){
lock();
return;
}
// flag用於判斷是否超時
long flag = mills;
// timeout表示超時時間
long timeout = System.currentTimeMillis()+mills;
// 如果鎖被持有,就讓其等待
while (LOCK){
if (flag<=0){
throw new TimeOutException(Thread.currentThread().getName()+"Time out");
}
blockedThreads.add(Thread.currentThread());
this.wait();
flag = timeout-System.currentTimeMillis();
}
// 拿到鎖
this.LOCK=true;
blockedThreads.remove(Thread.currentThread());
this.currentThread = Thread.currentThread();
}
@Override
synchronized public void unlock() {
// 如果鎖被人持有,並且當前試圖解鎖的也是當前線程,那就釋放鎖
while (LOCK && this.currentThread==Thread.currentThread()){
System.out.println("鎖已被釋放");
this.notifyAll();
LOCK=false;
}
// 如果鎖處於釋放狀態,那就不做操作
}
@Override
public Collection<Thread> getBlockedThreads() {
// unmodifiableCollection 在返回的過程中,不允許對其進行修改
return Collections.unmodifiableCollection(blockedThreads);
}
@Override
public int getBlockedSize() {
return blockedThreads.size();
}
}
客戶端測試
package concurrency.chapter8;
import com.sun.org.apache.xpath.internal.operations.Bool;
import java.util.stream.Stream;
public class LockTest {
public static void main(String[] args) {
Lock lock = new BooleanLock();
// 普通lock測試
Stream.of("T1","T2","T3")
.forEach(name->{
new Thread(()->{
// 當前線程拿到鎖,開始工作
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+" 拿到鎖");
work();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//結束後釋放鎖
lock.unlock();
}
},name).start();
});
// main函數視圖釋放鎖,但是我們有驗證,實際操作不了
lock.unlock();
// 超時lock測試
Stream.of("T4","T5","T6")
.forEach(name->{
new Thread(()->{
// 當前線程拿到鎖,開始工作
try {
lock.lock(100L);
System.out.println(Thread.currentThread().getName()+" 拿到鎖");
work();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Lock.TimeOutException e) {
System.out.println(Thread.currentThread().getName()+" 超時!");
} finally {
//結束後釋放鎖
lock.unlock();
}
},name).start();
});
// main函數視圖釋放鎖,但是我們有驗證,實際操作不了
lock.unlock();
}
// 模擬工作
private static void work(){
try {
System.out.println(Thread.currentThread().getName()+" 正在工作");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
15. 鉤子方法處理系統退出工作
當系統被終止的時候,往往還有很多連接資源沒有關閉,比如數據庫連接、網絡連接等等,因此我們在終止程序的時候,需要一些關閉各種資源的操作——用鉤子方法。
package concurrency.chapter9;
public class HookToExit {
public static void main(String[] args) {
// 鉤子方法
Runtime.getRuntime().addShutdownHook(new Thread(()->{
System.out.println("系統正在退出");
//進行退出資源回收
nofigyAndRelease();
}));
System.out.println("開始工作");
while (true){
}
}
private static void nofigyAndRelease() {
//這裏處理資源回收或通知
System.out.println("關閉緩存");
System.out.println("關閉數據源連接");
System.out.println("關閉socket");
System.out.println("系統已退出");
}
}
也能在nofigyAndRelease可以捕獲異常
上述代碼在linux下測試可以看到清楚的效果。
無論是手動shutdowm還是kill進程,都能處理退出工作。
注意kill -9 是強制終結命令,無法完成退出處理工作。
16. ThreadException與stackTrace(瞭解)
package concurrency.chapter9;
import java.util.Optional;
import java.util.stream.Stream;
public class ThreadException {
private static int A=1;
private static int B=0;
public static void main(String[] args){
Thread t = new Thread(()->{
// 這個異常無法拋出,只能try-catch
try {
Thread.sleep(100);
A/=B;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
// 這裏捕獲異常並處理
t.setUncaughtExceptionHandler((group,e)->{
System.out.println(group);
System.out.println(e);
});
System.out.println("打印方法調用棧");
Stream.of(Thread.currentThread().getStackTrace())
.filter(e->!e.isNativeMethod())
.forEach(e-> Optional.of("類名:"+e.getClassName()+" 方法名:"+e.getMethodName()+" 行數:"+e.getLineNumber()).ifPresent(System.out::println));
}
}
17. ThreadGroup
如果不設置group,會將線程加到當前group中。
ThreadGroup是樹形結構。
不能跨ThreadGroup訪問(既不能訪問父group,也不能訪問兄弟group)
group.destroy(),銷燬線程組,如果期內還有活躍的線程,拋出異常。
group.enumerate(Thread[] list【, boolean recurse】);
拷貝線程到list中,recurse指定是否需要對其內的其他線程組進行遞歸拷貝(深拷貝)
不加recurse則默認爲true
group.interrupt() 打斷其內的所有線程(遞歸式)
group.setDaemon()
線程組的Daemon跟線程不一樣,線程組的Daemon指的是,如果線程組被destroy或其內的所有線程都執行結束,那就銷燬該線程。
也就是說,非Daemon線程組需要手動回收
如果創建ThreadGroup時制定了parentThreadGroup,則Daemon與parent一致。
18. 手寫線程池
概念:
1.任務隊列:所有執行線程去任務隊列裏面拿任務執行
2.線程隊列:線程池有一個線程隊列
3.拒絕策略(拋出異常、丟棄、阻塞、臨時隊列):當任務數量超過線程池設定的可接受數量時,進行拒絕的處理策略
4.容量:線程池的線程隊列個數可以動態變化。
package chapter10;
import java.util.*;
// 線程池的簡單實現
public class ThreadPool extends Thread {
//定義線程池大小
private int size;
//定義線程池擴容最大容量
private final static int MAXZISE=30;
//可提交任務的最大值
private final int taskQueueMaxSize;
//線程池默認大小
public final static int DEFAULT_SIZE=10;
//任務隊列,用於存放外部傳進來的所有任務,執行一個就刪除一個
private final static LinkedList<Runnable> TASK_QUEUE = new LinkedList<>();
// 默認的任務最大個數
public final static int DEFAULT_TASK_QUEUE_SIZE=2000;
// 用於存放線程池的所有線程
private final static List<WorkerTask> WORKER_THREAD_QUEUE = new ArrayList<>();
// 用於線程命名自增
private static volatile int seq;
// 用於線程命名前綴
private final static String THREAD_PREFIX = "THREAD_POOL-";
private final static ThreadGroup GROUP = new ThreadGroup("THREAD_POOL");
// 默認的拒絕策略,超過數量直接拋出異常
public final static DiscardPolicy DEFAULT_DISCARD_POLICY = ()->{
throw new DiscardException("提交任務數量過多,任務被拒絕!");
};
// 提供給外部傳入
private DiscardPolicy discardPolicy;
// 線程池的狀態
private boolean isDead;
//空構造,設爲默認大小
public ThreadPool(){
this(DEFAULT_SIZE,DEFAULT_TASK_QUEUE_SIZE,DEFAULT_DISCARD_POLICY);
}
public ThreadPool(int size, int taskQueueMaxSize, DiscardPolicy discatdPolicy){
this.size=size;
this.taskQueueMaxSize = taskQueueMaxSize;
this.discardPolicy = discatdPolicy;
// 初始化線程池
init();
}
private void init() {
// 初始化創建size個工作線程
for (int i=0;i<size;i++){
createWorkTask();
}
this.start();
}
// 對外提供接口,加入任務
public void submit(Runnable runnable){
// 隊列入口,搶鎖
synchronized (TASK_QUEUE){
// 拒絕策略判斷,任務隊列的個數大於最大值
if (TASK_QUEUE.size()> taskQueueMaxSize){
discardPolicy.discard();
}
TASK_QUEUE.addLast(runnable);
// 加入隊列之後就通知其他休眠的(類似生產者消費者模型)
TASK_QUEUE.notifyAll();
}
}
// 線程池的狀態監控:擴容、縮容等
@Override
public void run(){
while (!isDead){
// 擴容
extend();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 縮容
unextend();
}
}
// 擴容
private void extend(){
synchronized (WORKER_THREAD_QUEUE){
// 如果提交的任務個數TASK_QUEUE.size()/2 > WROKER_THREAD_QUEUE,就再創建一個工作線程
if (TASK_QUEUE.size()<<1 > WORKER_THREAD_QUEUE.size() && WORKER_THREAD_QUEUE.size()<MAXZISE){
System.out.println("擴容前:size:"+size);
createWorkTask();
size++;
System.out.println("擴容後:size:"+size);
}
}
}
//縮容
private void unextend(){
synchronized (WORKER_THREAD_QUEUE){
// 如果提交的任務個數TASK_QUEUE.size()/2 < WROKER_THREAD_QUEUE,就減少一個工作線程
if (TASK_QUEUE.size()<<1 < WORKER_THREAD_QUEUE.size() && TASK_QUEUE.size()>0){
System.out.println("縮容前:size:"+size);
for (Iterator<WorkerTask> iterator = WORKER_THREAD_QUEUE.iterator();iterator.hasNext();){
WorkerTask task = iterator.next();
if (task.state==TaskState.BLOCKED){
task.interrupt();
task.close();
size--;
System.out.println("縮容後:size:"+size);
}
}
}
}
}
// 創建自定義的工作線程
private void createWorkTask(){
WorkerTask task = new WorkerTask(GROUP,THREAD_PREFIX+(seq++));
// 線程池中的線程在線程池創建後就被啓動,具體的狀態根據任務需求會自動更改
task.start();
// 加到工作隊列中
WORKER_THREAD_QUEUE.add(task);
}
// 拒絕策略,提供接口,讓外部也能實現自定義拒絕策略
public interface DiscardPolicy{
void discard() throws DiscardException;
}
// 通過拋出異常,拋出拒絕
public static class DiscardException extends RuntimeException{
public DiscardException(String msg){
super(msg);
}
}
// 關閉線程池
public void shutdown() throws InterruptedException {
// 如果任務隊列還有,那就稍等一會
while (!TASK_QUEUE.isEmpty()){
Thread.sleep(1);
}
// 如果任務隊列裏面沒有任務了,那就結束,需要循環結束
int initVal = WORKER_THREAD_QUEUE.size();
while (initVal>0){
for (WorkerTask task :
WORKER_THREAD_QUEUE) {
//如果任務執行完畢,那麼狀態必爲阻塞
if (task.state==TaskState.BLOCKED) {
task.close(); //state設爲DADE
task.interrupt();//打斷,退出任務循環
initVal--;
// 如果不是,就先等待一下,不要瘋狂運行
}else {
System.out.println(TASK_QUEUE.size());
Thread.sleep(1);
}
}
}
isDead=true;
System.out.println("--------------線程池已被關閉----------------");
}
// 定義任務狀態,空閒、運行、阻塞、死亡
private enum TaskState {
FREE,RUNNING,BLOCKED,DEAD
}
// 封裝Thread對象,讓其擁有我們定義的任務狀態等其他信息
private static class WorkerTask extends Thread{
//初始化爲空閒狀態
private volatile TaskState state = TaskState.FREE;
// 獲取任務狀態
public TaskState getTaskState(){
return this.state;
}
//調用父類的group的構造方法
public WorkerTask(ThreadGroup group,String name){
super(group,name);
}
//run,讓其執行完之後不能銷燬,而是放回池中
@Override
public void run() {
//如果線程狀態沒有死亡,就去隊列中獲取任務,保證工作線程在系統運行期間永不消亡
OUTER:
while (this.state!=TaskState.DEAD){
// 聲明任務
Runnable runnable;
//出隊,搶鎖
synchronized (TASK_QUEUE){
// 如果隊列爲空,就讓線程等待,釋放鎖
while (TASK_QUEUE.isEmpty()){
try {
// 進入wait,修改狀態
state = TaskState.BLOCKED;
TASK_QUEUE.wait();
} catch (InterruptedException e) {
//如果線程被打斷,就重新去獲取任務,保證工作線程永不消亡
break OUTER;
}
}
// 如果隊列不爲空,就拿到第一個任務
runnable = TASK_QUEUE.removeFirst();
}
// 拿到任務釋放鎖,開始工作
if (runnable!=null){
// 開始執行,修改狀態
state = TaskState.RUNNING;
runnable.run();
// 執行完畢,修改狀態
state = TaskState.FREE;
}
}
}
// 關閉任務
public void close(){
this.state = TaskState.DEAD;
}
}
public static void main(String[] args) throws InterruptedException {
// 初始化一個線程池
ThreadPool pool = new ThreadPool();
// 提交40個任務
for (int i = 0; i < 40; i++) {
pool.submit(()->{
System.out.println("任務被線程"+Thread.currentThread().getName()+"執行");
try {
// 模擬工作
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任務被線程"+Thread.currentThread().getName()+"執行完畢");
});
}
// 等待線程工作
Thread.sleep(20000);
pool.shutdown();
}
}
未完待續