【JavaEE】-- 多線程(初階)4

文章目錄

  • 8.多線程案例
    • 8.1 單例模式
      • 8.1.1 餓漢模式
      • 8.1.2 懶漢模式
    • 8.2 阻塞隊列
      • 8.2.1 什么是阻塞隊列
      • 8.2.2 生產者消費者模型
      • 8.2.3 標準庫中的阻塞隊列
      • 8.2.4 阻塞隊列的應用場景
        • 8.2.4.1 消息隊列
      • 8.2.5 異步操作
      • 8.2.5 自定義實現阻塞隊列
      • 8.2.6 阻塞隊列--生產者消費者模型``
    • 8.3 定時器
      • 8.3.1 標準庫中的定時器
      • 8.3.2 實現定時器
    • 8.4 線程池
      • 8.4.1 線程池是什么
      • 8.4.2 為什么要使用線程池
      • 8.4.3 標準庫中的線程池
      • 8.4.4 自定義一個線程池
      • 8.4.5 創建系統自帶的線程池
      • 8.4.6 線程池流程圖
      • 8.4.7 拒絕策略
  • 9. 總結-保證線程安全的思路
  • 10. 對比線程和進程
    • 10.1 線程的優點
    • 10.2 進程與線程的區別
  • 11. wait() 和 sleep()的區別

8.多線程案例

8.1 單例模式

單例模式是校招中最常考的設計模式之一。

什么是單例?
在程序中一個類只需要有一個對象實例。

什么是設計模式?
設計模式是對常見的業務場景總結出來的處理方法,可以將設計模式理解為解決某個問題時限制了邊界,同時限制了程序員的下限。
在這里插入圖片描述
1. JVM中哪些類只有一個對象?
類對象:.class文件被加載到JVM中以后,會創建一個描述類結構的對象,稱之為類對象,全局唯一

在Java中可以通過 .class 獲取到類對象。

static關鍵字修飾的屬性,在該類所有實例對象中共享。

static 代碼塊在類加載的時候執行;不帶static修飾的代碼塊,每new 一個對象都執行一次。

Java程序運行過程:

  1. 從磁盤加載 .class 文件到JVM,同時生成一個類對象。
  2. 創建實例變量

8.1.1 餓漢模式

實現過程:

  1. 要實現單例類,只需要定義一個static修飾的變量,就可以保證這個變量全局唯一(單例)。
    在這里插入圖片描述
  2. 既然是單例,就不想讓外部去new這個對象,雖然返回的是同一個對象,已經實現了單例,但是在代碼書寫上有歧義。
public class Singleton {//定義一個類的成員變量,用static修飾,保證全局唯一private static Singleton instance = new Singleton();public  Singleton getInstance() {return instance;}
}
public class Demo01 {public static void main(String[] args) {Singleton instance1 = new Singleton();System.out.println(instance1.getInstance());Singleton instance2 = new Singleton();System.out.println(instance2.getInstance());Singleton instance3 = new Singleton();System.out.println(instance3.getInstance());}
}

輸出結果:
在這里插入圖片描述

  1. 構造方法私有化
    在這里插入圖片描述
    在這里插入圖片描述
    這樣從語法上就不能再new對象了。

  2. 把獲取對象的方法改為static 通過類名.方法名的方式調用。

在這里插入圖片描述
在這里插入圖片描述
輸出結果:
在這里插入圖片描述
我們把這種類加載的時候就完成對象初始化的創建方式稱為“餓漢模式”。

由于程序在啟動的時候可能需要加載很多的類。單例類,并不一定要在程序啟動的時候用,為了節省計算機資源,加快程序的啟動,可以讓單例類在用到的時候在進行初始化。在編程中延時加載是一個褒義詞。

8.1.2 懶漢模式

  1. 只聲明這個全局變量,不初始化。
public class SingletonLazy {//定義一個類的成員變量,用static修飾,保證全局唯一private static SingletonLazy instance = null;
}
  1. 在 獲取單例對象的時候加一個是否為空的判斷,若為空則創建對象。
    在這里插入圖片描述

  2. 多次獲取對象,打印對象結果。(單線程)

public class Demo03 {public static void main(String[] args) {SingletonLazy instance1 = SingletonLazy.getInstance();System.out.println(instance1);SingletonLazy instance2 = SingletonLazy.getInstance();System.out.println(instance2);SingletonLazy instance3 = SingletonLazy.getInstance();System.out.println(instance3);}
}

輸出結果:
在這里插入圖片描述

  1. 測試在多線程環境中的運行結果
public class Demo02 {public static void main(String[] args) {for (int i = 0; i < 10; i++) {Thread thread = new Thread(()->{SingletonLazy instance = SingletonLazy.getInstance();System.out.println(instance);});thread.start();}}
}

輸出結果:
在這里插入圖片描述
可以看到在多線程中出現了線程安全問題,不再是單例對象了。
在這里插入圖片描述
分析出現線程安全問題的原因:在這里插入圖片描述

當t1LOAD時,instance 為NULL,執行完t1 的LOAD之后,被CPU調度到了 t2,我們假設CPU一次把 t2 的指令全部執行完,當執行完 t2 的最后一個指令STORE(將創建的instance對象寫回到了主內存中)。又被CPU調度到了 t1 此時已經執行完了LOAD,已經進入了if語句,所以就會直接執行下面的NEW操作,就又創建了一個新的對象。當 t1 的指令執行到STORE,就會把在 t1 新創建的instance 寫入到主內存中,會將在 t2 中創建的 instance 覆蓋掉,這樣就造成了線程安全問題。

給內層加鎖
在這里插入圖片描述
分析給內層加鎖不能解決線程安全問題的原因:
在這里插入圖片描述
我們假設CPU先執行完 t1 的 LOAD 和 判斷操作,此時已經執行完了判斷操作,并且此時 instance 為NULL,已經進入了 if 語句。但是接下來被CPU調度到了 t2 ,我們假設 t2 中的所有指令執行完,才被CPU再次調回了 t1 ,t2 中將instance對象寫入到了主內存中,并釋放了鎖之后,此時已經進入到了 if 語句,t1 拿到了鎖,就會執行下面的創建 instance 對象的操作,此時又創建了一個新的instance對象,然后被寫入到了主內存中,覆蓋掉了t2中創建的instance對象。此時,線程安全問題依舊存在。

給外層加鎖
在這里插入圖片描述
分析給外層加鎖解決線程安全問題的原因:
在這里插入圖片描述
給外層加鎖和給內存加鎖最大的不一樣就是,給內層加鎖是先進入 if 語句再競爭鎖,還是先競爭鎖再進入if 語句。
我們假設t1 先競爭到了鎖,執行到了判斷指令,此時 t1 已經進入到了 if 語句,然而被CPU調度到了t2,此時t2想要拿到鎖,但是此時鎖還被t1 拿著, t1 并沒有釋放鎖,直到再次被CPU調度回 t1 ,直到執行完UNLOCK,此時已經創建了 instance 對象,并將其寫入到了主內存中,當再次被CPU調度到 t2 時,它指向判斷操作時,已經發現instance對象不為NULL,所以它就進不去if語句,就修改不了instance。所以,線程安全問題得以解決。

給外層加鎖的另一個小問題:

  1. 當第一個線程進入getInstance 方法時,如果線程還沒有初始化,則獲取鎖進行初始化操作,此時單例對象被第一個線程創建完成。
  2. 給外層加鎖時,一旦有一個線程獲取到了鎖,那么這個線程就會創建 instance 對象,后面再競爭到鎖的線程就永遠不會進入 if 語句。
  3. 那么后面的競爭鎖的行為就都是對資源的一種消耗,LOCK和UNLOCK對應的鎖指令是互斥鎖,比較消耗系統資源

解決問題:
我們在加鎖前再去判斷一下是否需要加鎖。
在這里插入圖片描述
我們把這種叫做雙重檢查鎖(DCL)
在這里插入圖片描述
解決內存可見性和指令重排序問題:
在這里插入圖片描述
在這里插入圖片描述
DCL的方式必須要學會手寫,面試中如果手寫代碼,必考!!!
面試中使用DCL,工作中使用“餓漢式”

8.2 阻塞隊列

8.2.1 什么是阻塞隊列

阻塞隊列是?種特殊的隊列.也遵守"先進先出"的原則.
阻塞隊列能是?種線程安全的數據結構,并且具有以下特性:
? 當隊列滿的時候,繼續?隊列就會阻塞,直到有其他線程從隊列中取?元素.
? 當隊列空的時候,繼續出隊列也會阻塞,直到有其他線程往隊列中插?元素.
阻塞隊列的?個典型應?場景就是"?產者消費者模型".這是?種?常典型的開發模型.

8.2.2 生產者消費者模型

?產者消費者模式就是通過?個容器來解決?產者和消費者的強耦合問題。
?產者和消費者彼此之間不直接通訊,?通過阻塞隊列來進?通訊,所以?產者?產完數據之后不?等待消費者處理,直接扔給阻塞隊列,消費者不找?產者要數據,?是直接從阻塞隊列?取.

  1. 阻塞隊列就相當于?個緩沖區,平衡了?產者和消費者的處理能?.(削峰填?)

?如在"秒殺"場景下,服務器同?時刻可能會收到?量的?付請求.如果直接處理這些?付請求,服務器可能扛不住(每個?付請求的處理都需要?較復雜的流程).這個時候就可以把這些請求都放到?個阻塞隊列中,然后再由消費者線程慢慢的來處理每個?付請求.
這樣做可以有效進?"削峰",防?服務器被突然到來的?波請求直接沖垮.

在這里插入圖片描述

8.2.3 標準庫中的阻塞隊列

在這里插入圖片描述

public class Demo0 {public static void main(String[] args) throws InterruptedException {BlockingQueue queue = new LinkedBlockingQueue(3);queue.put(1);queue.put(2);queue.put(2);System.out.println("隊列已滿.....");queue.put(4);System.out.println("4不會被執行....");}
}

輸出結果:
在這里插入圖片描述

public class Demo0 {public static void main(String[] args) throws InterruptedException {BlockingQueue queue = new LinkedBlockingQueue(3);queue.put(1);queue.put(2);queue.put(2);System.out.println("隊列已滿.....");System.out.println(queue.take());System.out.println(queue.take());System.out.println(queue.take());System.out.println("已經取出三個元素....");System.out.println(queue.take());System.out.println("已經取出四個元素");}
}

輸出結果:
在這里插入圖片描述

8.2.4 阻塞隊列的應用場景

8.2.4.1 消息隊列

在這里插入圖片描述
在這里插入圖片描述
問:如何判斷消息是發給服務器A、服務器B還是服務器C?
答:服務器A在生產消息的時候,可以打一個標簽,相當于對消息進行了分類,消費者在獲取消息時,可以根據這個標簽來獲取。

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

  1. 阻塞隊列也能使?產者和消費者之間解耦.

?如過年?家??起包餃?.?般都是有明確分?,?如?個?負責搟餃??,其他?負責包.搟餃??的?就是"?產者",包餃?的?就是"消費者".
搟餃??的?不關?包餃?的?是誰(能包就?,?論是??包,借助?具,還是機器包),包餃?的?也不關?搟餃??的?是誰(有餃??就?,?論是?搟?杖搟的,還是拿罐頭瓶搟,還是直接從超市買的).

8.2.5 異步操作

在這里插入圖片描述

8.2.5 自定義實現阻塞隊列

自定義實現的阻塞隊列:

public class MyBlockingQueue {//定義一個數組來存放數據,具體的容量由構造方法中的參數決定private Integer[] elementData;//定義頭尾下標private volatile int head;private volatile int tail;//定義數組中元素的個數private volatile int size = 0;//構造public MyBlockingQueue(int capacity){if (capacity <= 0){//處理輸入不合法throw new RuntimeException("隊列容量必須大于0");}elementData = new Integer[capacity];}// 插入---給代碼塊加鎖public void put(Integer value) throws InterruptedException {synchronized (this){//判滿if (size >= elementData.length){//阻塞隊列在隊列滿的時候應該阻塞等待this.wait();//wait操作釋放鎖}//插入數據elementData[tail] = value;tail++;size++;//隊列中有元素了,喚醒阻塞等待的線程synchronized (this){this.notifyAll();}//處理隊尾下標if (tail >= elementData.length){tail = 0;}}}//獲取數據---給方法加鎖public synchronized Integer take() throws InterruptedException {//判空if (size == 0){//隊列空的時候阻塞隊列應該阻塞等待this.wait();}//獲取數據Integer value = elementData[head];head++;size--;//隊列中有空的位置了,喚醒阻塞隊列的線程this.notifyAll();//處理隊頭下標if (head >= elementData.length){head = 0;}return value;}
}

測試加入元素:

public class Demo01 {public static void main(String[] args) throws InterruptedException {MyBlockingQueue queue = new MyBlockingQueue(3);queue.put(1);queue.put(2);queue.put(3);System.out.println("已經加入三個元素....");queue.put(4);System.out.println("已經加入四個元素....");}
}

輸出結果:
在這里插入圖片描述
測試取出元素:

public class Demo02 {public static void main(String[] args) throws InterruptedException {MyBlockingQueue queue = new MyBlockingQueue(3);queue.put(1);queue.put(2);queue.put(3);queue.take();queue.take();queue.take();System.out.println("已經取出三個元素....");queue.take();System.out.println("已經取出四個元素....");}
}

輸出結果:
在這里插入圖片描述
如果在put元素的時候,隊列滿了,積壓了很多線程,當size–之后,就會有不止一個線程去put元素,就會出現還沒有出隊元素被覆蓋的情況。為了解決這個問題我就需要把判滿的 if 換成 while,讓被喚醒之后的線程重新判斷一次這個條件。
在這里插入圖片描述

8.2.6 阻塞隊列–生產者消費者模型``

public class Demo03 {public static void main(String[] args) {MyBlockingQueue queue = new MyBlockingQueue(100);//創建生產者線程Thread producer = new Thread(()->{int num = 0;while (true){try {//添加元素queue.put(num);System.out.println("生產了元素:" + num);num++;//休眠一會:10毫秒TimeUnit.MILLISECONDS.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}}});producer.start();//定義一個消費者線程Thread comsumer = new Thread(()->{//不斷的從隊列取出元素while (true) {try {//取出元素Integer value = queue.take();System.out.println("消費了元素:" + value);//休眠1秒TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}}});comsumer.start();}
}

輸出結果:
在這里插入圖片描述
生產者先把隊列生產滿,生產滿了之后消費一個生產一個。

8.3 定時器

8.3.1 標準庫中的定時器

在這里插入圖片描述
那么這個task任務究竟是怎樣的呢?
在這里插入圖片描述
我們追溯源碼發現這個方法實現了Runnable接口。
在這里插入圖片描述
而且里面有一個沒有實現的抽象方法run()方法。我們就可以通過它來定義自己的任務。

public class Demo01 {public static void main(String[] args) {//根據JDK中提供的類,創建一個定時器Timer timer = new Timer();//向定時器中添加任務timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("該起床了.....");}}, 1000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("任務2.....");}}, 3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("任務3.....");}}, 5000);}
}

輸出結果:
在這里插入圖片描述
執行完已有任務之后,就阻塞等待新任務。

8.3.2 實現定時器

在這里插入圖片描述

public class MyTimer {//用一個阻塞隊列來組織任務private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();//提供一個方法,提交任務public void schedule(Runnable runnable, long delay){//根據傳入的參數,構造一個MyTaskMyTask task = new MyTask(runnable, delay);//把任務放入阻塞隊列try {queue.put(task);} catch (InterruptedException e) {throw new RuntimeException(e);}}public MyTimer(){//創建掃描線程Thread thread = new Thread(()->{//不斷的掃描隊列中的任務while (true){//1. 取出任務try {MyTask task = queue.take();//2. 判斷執行時間到了嗎long currentTime = System.currentTimeMillis();if (currentTime >= task.getTime()){//時間到了,執行任務task.getRunnable().run();}else {//沒有到時間,重新放回隊列queue.put(task);}} catch (InterruptedException e) {throw new RuntimeException(e);}}});thread.start();}
}//用一個類來描述任務及任務執行的時間
class MyTask implements Comparable<MyTask>{//任務private Runnable runnable;//任務執行的時間private long time;public MyTask(Runnable runnable, long delay) {//校驗任務不能為空if (runnable == null){throw new IllegalArgumentException("任務不能為空");}//時間不能為負數if (delay < 0){throw new IllegalArgumentException("時間不能為負數");}this.runnable = runnable;//計算出任務執行的具體時間this.time = delay + System.currentTimeMillis();}public Runnable getRunnable() {return runnable;}public long getTime() {return time;}@Overridepublic int compareTo(MyTask o) {//為了解決可能會溢出的問題,我們不使用相減的方式,使用比較的方式if (this.getTime() > o.getTime()){return 1;} else if (this.getTime() < o.getTime()) {return -1;}else {return 0;}//return (int) (this.time - o.getTime());//小根堆,小的在前}
}
public class Demo02 {public static void main(String[] args) {//創建一個定時器對象MyTimer timer = new MyTimer();//添加任務timer.schedule(()->{System.out.println("該起床了");},1000);timer.schedule(()->{System.out.println("任務2");},2000);timer.schedule(()->{System.out.println("任務3");},3000);timer.schedule(null, -10);}
}

輸出結果:
在這里插入圖片描述

問題1:忙等
在這里插入圖片描述

在這里插入圖片描述
假如當前的時間為18:52,判斷出距離我們的隊列中下一個要執行的任務時間還差一個小時,那么我們就會再次把這個任務放回隊列中,在這一個小時中,構造方法中的while循環一直在循環執行,這個現象叫忙等,浪費了計算機的資源。

我們發現,放回隊列的操作是導致忙等問題等問題的代碼,為了解決這個問題,我們可以在放回隊列時讓程序等待一段時間,等待的時間為下一個任務的執行時間和當前時間的差。

在這里插入圖片描述
問題2:添加新任務之后的第一個要執行的任務的時間變了

在這里插入圖片描述
上一個問題解決了之后,在這個等待的時間里,我們可能會添加新的任務,假設我們添加了任務3,那么我們就會做不到定時執行任務。

為了解決這個問題,我們可以在當向隊列中新添加任務時,統一喚醒一次線程,這樣就能 保證能夠掃描到新添加進去的線程,不會超時執行任務。
在這里插入圖片描述

問題3:基于線程搶占式執行,由于CPU調度的問題產生的一系列現象
CPU調度的過程中可能會產生執行順序的問題,或當一個線程執行到一半的時間被調度走的現象。
在這里插入圖片描述
在執行上面這段代碼我們假設該線程t1執行完MyTask task = queue.take();之后就被CPU調度走了,被調度走去執行主線程t2中的任務,我們假設主線程又添加了一個新的任務,執行下面這段代碼,直執行完下面的代碼,才被CPU重新調度回原來的線程t1。
在這里插入圖片描述
線程t1得到CPU資源之后繼續執行后面的代碼
在這里插入圖片描述
那么可能會出現下面的問題,由于線程調度的問題,t2先入隊了新任務,執行事件中愛t1讀取的任務執行時間之間,t1讀的任務發現時間沒有到放回隊列的時候,設置的等待時間超過了新任務的執行時間,導致t2放入隊列的新任務不能即使的執行。造成這個現象的原因是沒有保證原子性

為了解決上面的問題,我們需要擴大鎖的范圍。

//構造方法public MyTimer(){//創建掃描線程Thread thread = new Thread(()->{//不斷的掃描隊列中的任務while (true){//1. 取出任務try {synchronized (this){//wait和notify必須搭配synchronized使用MyTask task = queue.take();//2. 判斷執行時間到了嗎long currentTime = System.currentTimeMillis();if (currentTime >= task.getTime()){//時間到了,執行任務task.getRunnable().run();}else {//當前時間與執行任務時間的差long waitTime = task.getTime() - currentTime;//沒有到時間,重新放回隊列queue.put(task);//加入等待時間this.wait(waitTime);}}} catch (InterruptedException e) {throw new RuntimeException(e);}}});thread.start();}

在這里插入圖片描述
這樣就解決了原子性的問題。


再來看接下來的代碼:如果我們添加的任務延遲時間都是0呢?

public class Demo02 {public static void main(String[] args) throws InterruptedException {//創建一個定時器對象MyTimer timer = new MyTimer();//添加任務timer.schedule(()->{System.out.println("該起床了");},0);timer.schedule(()->{System.out.println("任務2");},0);timer.schedule(()->{System.out.println("任務3");},0);}
}

輸出結果:
在這里插入圖片描述
此時線程就又出現了問題。

在多線程環境中出現的問題,一定要使用線程查看工具去觀察線程的狀態。
在這里插入圖片描述

上面顯示第37行被鎖定,我們就找一下第37行
在這里插入圖片描述
當代碼執行到這一行時,要從隊列中取任務,但是當隊列中沒有任務的時候,就會阻塞等待,一直到隊列中有可用元素才會執行。

在這里插入圖片描述

  1. 提交任務1
  2. 掃描線程取出任務執行
  3. while循環繼續執行任務,但是現在隊列中沒有任務可用,于是就阻塞等待。

在這里插入圖片描述

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

8.4 線程池

只要面試問到多線程,必問!!!

8.4.1 線程池是什么

其實就是字面意思,一次創建很多個線程,用的時候從池子里拿一個出來,用完之后還回池子。

8.4.2 為什么要使用線程池

避免了頻繁創建銷毀線程的開銷,提升程序的性能。

在數據庫中就有一個DataSource數據源,一開始就初始化了很多個數據庫連接,當需要用數據庫連接的時候,從池子中獲取一個連接,用完之后換回池子,并不真正的銷毀連接。

線程池中的線程不停的掃描保存任務中的集合,當有任務的時候執行任務,沒有任務的時候阻塞等待,但是并不銷毀線程。

為什么使用線程池可提升效率?
少量創建,少量銷毀。
內核態: 操作系統層面。
**用戶態:**JVM層面(應用程序層)
在這里插入圖片描述

8.4.3 標準庫中的線程池

在這里插入圖片描述
需要背一下,面試中可能會問JDK中提供了幾種線程池。

在使用線程池時,我們只需要定義好任務,并提交給線程池即可,線程是池子自動創建的。

在這里插入圖片描述
這是通過類名.方法名的方式獲取對象,那么可不可以通過new的方式去獲取對象?
當然可以,但是構造方法不能完整的覆蓋業務的需要。

public class Student {private int id;private  int age;private  int classId;private String name;private String sno;//通過age 和 name 初始化一個對象public Student(int age, String name){this.age = age;this.name = name;}//通過classId 和 name 初始化一個對象public Student (int classId, String name){this.classId = classId;this.name = name;}public int getId() {return id;}public void setId(int id) {this.id = id;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public int getClassId() {return classId;}public void setClassId(int classId) {this.classId = classId;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getSno() {return sno;}public void setSno(String sno) {this.sno = sno;}
}

在這里插入圖片描述
方法重載了,參數列表相同了,方法重載時要保證參數列表的類型和個數不同。

這個需求是真實存在的,但是語法限制,不能這么寫。

//通過age 和 name 初始化一個對象public static Student createStudentByAgeAndName(int age, String name){Student student = new Student();student.setAge(age);student.setName(name);return student;}//通過classId 和 name 初始化一個對象public static Student createStudentByClassIdAndName(int classId, String name){Student student = new Student();student.setClassId(classId);student.setName(name);return student;}

這是一種工廠方法模式,根據不同的業務需求定義不同的方法獲取對象。

8.4.4 自定義一個線程池

思路:

  1. 用Runnable描述任務
  2. 組織管理任務可以使用一個隊列,可以用阻塞隊列去實現,使用阻塞隊列的好處是:當隊列中沒有任務的時候就等待,節省系統資源。
  3. 提供一個向隊列中添加任務的方法。
  4. 創建多個線程,掃描隊列中的任務,有任務的時候就取出來執行即可。

寫代碼的時候,要先整理思路,再動手實現。
MyThreadPool類:

public class MyThreadPool {//定義阻塞隊列來組織任務BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(100);//構造方法public MyThreadPool(int threadNum){if (threadNum < 0){throw new IllegalArgumentException("線程任務必須大于0");}//創建線程for (int i = 0; i < threadNum; i++) {Thread thread = new Thread(()->{//不停的掃描隊列while (true){try {//取出任務Runnable runnable = queue.take();//執行任務runnable.run();} catch (InterruptedException e) {throw new RuntimeException(e);}}});//啟動線程thread.start();}}/*** 提交任務到線程池* @param runnable 具體的任務* @throws InterruptedException*/public void submit(Runnable runnable) throws InterruptedException {if (runnable == null){throw new IllegalArgumentException("任務不能為空");}//把任務加入到隊列queue.put(runnable);}
}

測試類:

public class Demo01 {public static void main(String[] args) throws InterruptedException {//初始化一個自定義的線程池MyThreadPool threadPool = new MyThreadPool(3);//通過循環向線程中提交任務for (int i = 0; i <10; i++) {int taskId = i +1;threadPool.submit(()->{System.out.println("執行任務:" + taskId + Thread.currentThread().getName());});}}
}

輸出結果:

執行任務:1Thread-0
執行任務:2Thread-0
執行任務:3Thread-0
執行任務:4Thread-0
執行任務:5Thread-0
執行任務:6Thread-0
執行任務:8Thread-0
執行任務:9Thread-0
執行任務:7Thread-1
執行任務:10Thread-0

8.4.5 創建系統自帶的線程池

通過上面的工廠方法獲取的線程池比較固定,也就是說不能進行定制,在實際的開發過程中,使用的是定制性比較強的創建線程池的方式。
在這里插入圖片描述

面試題:說一說創建線程時的七個參數?

  1. 核心線程的數量
  2. 線程池中最大的線程數,最大線程數減去核心線程數 = 臨時線程數。
  3. 臨時線程的存活時間(一個數)。
  4. 臨時線程的存活時間的時間單位,它和第三個參數配合在一起就是臨時線程真正的存活時間
  5. 組織(保存)任務的隊列。
  6. 創建線程的工廠,不關注。
  7. 拒絕策略。

面試題:線程池的工作原理:
實例1:
周末去吃火鍋,火鍋店很火,去的晚了就需要排號。

  1. 火鍋店里有5張桌子(核心線程數)去了早了,店里沒人就可以直接上桌點菜。
  2. 越到飯點人越來越多,這時5張桌子都坐滿了,后面來的人就需要排號,最多可以排到20號(當于阻塞隊列,20相當于阻塞隊列的容量)。
  3. 排隊的人越來越多,已經排到20號了(阻塞隊列已經滿了),在外面加了10張臨時的桌子(臨時線程數,線程池中總的線程數 = 核心線程數 + 臨時線程數)。
  4. 排號的人就可以在外面的桌子上就餐。
  5. 時間越來越晚,排隊的人都已經就餐了,外面的桌子慢慢也空下來了,老板說再等30分鐘(臨時線程的存活時間,臨時線程的時間單位),如果再沒人來就把外面的桌子收掉。
  6. 收掉外面的桌子,店里的5張桌子(最后又回歸到了核心線程數)就可以滿足顧客的就餐要求。
  7. 中途如果排號滿了20號(阻塞隊列滿了),10張外面的桌子也坐滿了(線程數量達到了線程池的最大個數),老板就不接待客人了(拒絕策略)。
    實例2:去銀行辦業務
    在這里插入圖片描述
  8. 銀行平時只開兩上辦理業務的窗口,相當于線程池的核心線程數。
  9. 當有新客戶來銀時,看到開放的兩個容口空著,就可以直接去辦理業務。
  10. 當兩個窗口都有人在辦理業務,后進來的客戶就要去等待區等待。
  11. 隨著等待的人越來越多,等待區已經滿了,那么銀行就叫來其他的業務員來開放其他三個窗口,一起辦理業務。
  12. 再來銀行的客戶,就執行拒絕策略。

8.4.6 線程池流程圖

  1. 添加任務,核心線程從隊列中取任務去執行。
  2. 核心線程都在工作時,再添加的任務會進入到阻塞隊列。
  3. 阻塞隊列滿了之后,會創建臨時線程。
  4. 執行拒絕策略。

8.4.7 拒絕策略

在這里插入圖片描述

在這里插入圖片描述

  1. 直接拒絕
    在這里插入圖片描述
    比如公司給分配了一個任務,我說現在我很忙,沒有時間去處理這個任務,你就告訴領導說:你找別人干吧,我沒時間。
  2. 返回給調用者
    在這里插入圖片描述
    比如公司給分配了一個任務,我說現在我很忙,沒有時間去處理這個任務,你自己做吧。誰給我分配的任務我就把這個任務返回給誰,保證整個任務有線程執行。
  3. 放棄目前最早等待的任務
    在這里插入圖片描述

比如公司給分配了一個任務,我說現在我很忙,沒有時間去處理這個任務,老板說:最開始給你分的那個活,你可以不干了。
4. 放棄新提交的任務
在這里插入圖片描述
放棄的任務,以后也找不回來了,所以指定拒絕策略的時候,要關注任務是不是需要必須執行,如果必須執行,就指定“返回調用者”,否則1 3 4 選一個即可,1在拒絕后會拋出異常;3,4在拒絕后不會拋出異常。

  1. 直接拒絕
public class Demo02 {public static void main(String[] args) throws InterruptedException {//定義一個線程池ThreadPoolExecutor threadPool =new ThreadPoolExecutor(3,5,1,TimeUnit.SECONDS,new LinkedBlockingQueue<>(5),new ThreadPoolExecutor.AbortPolicy());//通過循環向線程池中提交任務for (int i = 0; i < 100; i++) {int taskId =  i + 1;threadPool.submit(()->{System.out.println("執行任務:" + taskId + ", " + Thread.currentThread().getName());});}}
}

輸出結果:
在這里插入圖片描述

  1. 放棄目前最早的任務
    在這里插入圖片描述
    輸出結果:
    在這里插入圖片描述
  2. 拋棄最新的任務
    在這里插入圖片描述
    輸出結果:
    在這里插入圖片描述
  3. 返回給調用者
    在這里插入圖片描述
    輸出結果:
    在這里插入圖片描述
    根據不同的業務場景選擇不同的拒絕策略

9. 總結-保證線程安全的思路

  1. 使用沒有共享資源的模型
  2. 使用共享資源,只讀不寫的模型
  • 不需要寫共享資源的模型
  • 使用不可變對象
  1. 直面線程安全(重點)
  • 保證原子性
  • 保證順序性
  • 保證可見性

10. 對比線程和進程

10.1 線程的優點

  1. 創建?個新線程的代價要?創建?個新進程?得多
  2. 與進程之間的切換相?,線程之間的切換需要操作系統做的?作要少很多
  3. 線程占?的資源要?進程少很多
  4. 能充分利?多處理器的可并?數量
  5. 在等待慢速I/O操作結束的同時,程序可執?其他的計算任務
  6. 計算密集型應?,為了能在多處理器系統上運?,將計算分解到多個線程中實現
  7. I/O密集型應?,為了提?性能,將I/O操作重疊。線程可以同時等待不同的I/O操作。

10.2 進程與線程的區別

  1. 進程是系統進?資源分配和調度的?個獨?單位,線程是程序執?的最?單位。
  2. 進程有??的內存地址空間,線程只獨享指令流執?的必要資源,如寄存器和棧。
  3. 由于同?進程的各線程間共享內存和?件資源,可以不通過內核進?直接通信。
  4. 線程的創建、切換及終?效率更?。

11. wait() 和 sleep()的區別

  1. 共同點,都會讓線程阻塞一會兒
  2. 從實現使用上來說是兩種不同的方式wait是Object類的方法,和鎖相關,配合synchronized一起使用,調用wait之后會釋放鎖sleep是Thread類的方法,與鎖無關.
  3. wait可以通過指定超時時間和通過notify方法喚醒,喚醒之后會重新競爭鎖資源sleep只能通過超時時間喚醒

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/896961.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/896961.shtml
英文地址,請注明出處:http://en.pswp.cn/news/896961.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【C++設計模式】第四篇:建造者模式(Builder)

注意&#xff1a;復現代碼時&#xff0c;確保 VS2022 使用 C17/20 標準以支持現代特性。 分步驟構造復雜對象&#xff0c;實現靈活裝配 1. 模式定義與用途 核心目標&#xff1a;將復雜對象的構建過程分離&#xff0c;使得同樣的構建步驟可以創建不同的表示形式。 常見場景&am…

vuex中的state是響應式的嗎?

在 Vue.js 中&#xff0c;Vuex 的 state 是響應式的。這意味著當你更改 state 中的數據時&#xff0c;依賴于這些數據的 Vue 組件會自動更新。這是通過 Vue 的響應式系統實現的&#xff0c;該系統使用了 ES6 的 Proxy 對象來監聽數據的變化。 當你在 Vuex 中定義了一個 state …

若依框架中的崗位與角色詳解

若依框架中的崗位與角色詳解 一、核心概念與定位 崗位&#xff08;Post&#xff09; 業務職能導向&#xff1a;崗位是用戶在組織架構中的職務標識&#xff08;如“開發人員”“項目經理”&#xff09;&#xff0c;用于描述工作職責而非直接控制權限。崗位與部門關聯&#xff…

SQL經典常用查詢語句

1. 基礎查詢語句 1.1 查詢表中所有數據 在SQL中&#xff0c;查詢表中所有數據是最基本的操作之一。通過使用SELECT * FROM table_name;語句&#xff0c;可以獲取指定表中的所有記錄和列。例如&#xff0c;假設有一個名為employees的表&#xff0c;包含員工的基本信息&#xf…

EP 架構:未來主流方向還是特定場景最優解?

DeepSeek MoE架構采用跨節點專家并行&#xff08;EP&#xff09;架構&#xff0c;在提升推理系統性能方面展現出巨大潛力。這一架構在發展進程中也面臨諸多挑戰&#xff0c;其未來究竟是會成為行業的主流方向&#xff0c;還是僅適用于特定場景&#xff0c;成為特定領域的最優解…

[密碼學實戰]Java實現國密(SM2)密鑰協商詳解:原理、代碼與實踐

一、代碼運行結果 二、國密算法與密鑰協商背景 2.1 什么是國密算法&#xff1f; 國密算法是由中國國家密碼管理局制定的商用密碼標準&#xff0c;包括&#xff1a; SM2&#xff1a;橢圓曲線公鑰密碼算法&#xff08;非對稱加密/簽名/密鑰協商&#xff09;SM3&#xff1a;密碼…

動漫短劇開發公司,短劇小程序搭建快速上線

在當今快節奏的生活里&#xff0c;人們的娛樂方式愈發多元&#xff0c;而動漫短劇作為新興娛樂形式&#xff0c;正以獨特魅力迅速崛起&#xff0c;成為娛樂市場的耀眼新星。近年來&#xff0c;動漫短劇市場呈爆發式增長&#xff0c;吸引眾多創作者與觀眾目光。 從市場規模來看…

第四十五:創建一個vue 的程序

html <div id"app">{{ msg }}<h2>{{ web.title }}</h2><h3>{{ web.url }}</h3> </div> js /*<div id"app"></div> 指定一個 id 為 app 的 div 元素{{ }} 插值表達式, 可以將 Vue 實例中定義的數據在視圖…

docer swarm集群部署springboot項目

1.準備兩臺服務器&#xff0c;安裝好docker、docker-compose 因為用到了docker倉庫&#xff0c;安裝harbor,可以從github下載離線安裝包 2. 我這邊用到了gitlab-ci,整體流程也都差不多 1&#xff09;打包mvn clean install 2&#xff09;打鏡像 docker-compose -f docker-compo…

Python測試框架Pytest的參數化

上篇博文介紹過&#xff0c;Pytest是目前比較成熟功能齊全的測試框架&#xff0c;使用率肯定也不斷攀升。 在實際工作中&#xff0c;許多測試用例都是類似的重復&#xff0c;一個個寫最后代碼會顯得很冗余。這里&#xff0c;我們來了解一下pytest.mark.parametrize裝飾器&…

開發博客系統

前言 準備工作 數據庫表分為實體表和關系表 第一&#xff0c;建數據庫表 然后導入前端頁面 創建公共模塊 就是統一返回值&#xff0c;異常那些東西 自己造一個自定義異常 普通類 mapper 獲取全部博客 我們只需要返回id&#xff0c;title&#xff0c;content&#xff0c;us…

【Spring Boot 應用開發】-05 命令行參數

Spring Boot 常用命令行參數 Spring Boot 支持多種命令行參數&#xff0c;這些參數可以在啟動應用時通過命令行直接傳遞。以下是一些常用的命令行參數及其詳細說明&#xff1a; 1. 基本配置參數 --server.port端口號 指定應用程序運行的HTTP端口&#xff0c;默認為8080。 jav…

20250304學習記錄

第一部分&#xff0c;先來了解一下各種論文期刊吧&#xff0c;畢竟也是這把歲數了&#xff0c;還什么都不懂呢 國際期刊&#xff1a; EI收集的主要有兩種&#xff0c; JA&#xff1a;EI源刊 CA&#xff1a;EI會議 CPCI也叫 ISTP 常說的SCI分區是指&#xff0c;JCR的一區、…

2024 年 MySQL 8.0.40 安裝配置、Workbench漢化教程最簡易(保姆級)

首先到官網上下載安裝包&#xff1a;http://www.mysql.com 點擊下載&#xff0c;拉到最下面&#xff0c;點擊社區版下載 windows用戶點擊下面適用于windows的安裝程序 點擊下載&#xff0c;網絡條件好可以點第一個&#xff0c;怕下著下著斷了點第二個離線下載 雙擊下載好的安裝…

網絡安全檢查漏洞內容回復 網絡安全的漏洞

網絡安全的核心目標是保障業務系統的可持續性和數據的安全性&#xff0c;而這兩點的主要威脅來自于蠕蟲的暴發、黑客的攻擊、拒絕服務攻擊、木馬。蠕蟲、黑客攻擊問題都和漏洞緊密聯系在一起&#xff0c;一旦有重大安全漏洞出現&#xff0c;整個互聯網就會面臨一次重大挑戰。雖…

汽車智能鑰匙中PKE低頻天線的作用

PKE&#xff08;Passive Keyless Entry&#xff09;即被動式無鑰匙進入系統&#xff0c;汽車智能鑰匙中PKE低頻天線在現代汽車的智能功能和安全保障方面發揮著關鍵作用&#xff0c;以下是其具體作用&#xff1a; 信號交互與身份認證 低頻信號接收&#xff1a;當車主靠近車輛時…

uiautomatorviewer定位元素報Unexpected ... UI hierarchy

發現問題 借鑒博客 Unexpected error while obtaining UI hierarchy android app UI自動化-元素定位輔助工具 Unexpected error while obtaining UI hierarchy&#xff1a;使用uiautomatorviewer定位元素報錯 最近在做安卓自動化,安卓自動化主要工作之一就是獲取UI樹 app端獲…

通俗的方式解釋“零錢兌換”問題

“零錢兌換”是一道經典的算法題目&#xff0c;其主要問題是&#xff1a;給定不同面額的硬幣和一個總金額&#xff0c;求出湊成總金額所需的最少硬幣個數。如果沒有任何一種硬幣組合能組成總金額&#xff0c;返回-1。 解題思路 動態規劃&#xff1a;使用動態規劃是解決零錢兌…

GBT32960 協議編解碼器的設計與實現

GBT32960 協議編解碼器的設計與實現 引言 在車聯網領域&#xff0c;GBT32960 是一個重要的國家標準協議&#xff0c;用于新能源汽車與監控平臺之間的數據交互。本文將詳細介紹如何使用 Rust 實現一個高效可靠的 GBT32960 協議編解碼器。 整體架構 編解碼器的核心由三個主要組…

Halcon 車牌識別-超精細教程

車牌示例 流程: 讀取圖片轉灰度圖閾值分割,找車牌內容將車牌位置設置變換區域形狀找到中心點和弧度利用仿射變換,斜切車牌旋轉轉正,把車牌摳出來利用形態學操作拼接車牌號數字訓練ocr開始識別中文車牌 本文章用到的算子(解析) Halcon 算子-承接車牌識別-CSDN博客 rgb1_to_gray…