文章目錄
- 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程序運行過程:
- 從磁盤加載 .class 文件到JVM,同時生成一個類對象。
- 創建實例變量
8.1.1 餓漢模式
實現過程:
- 要實現單例類,只需要定義一個static修飾的變量,就可以保證這個變量全局唯一(單例)。
- 既然是單例,就不想讓外部去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());}
}
輸出結果:
-
構造方法私有化
這樣從語法上就不能再new對象了。 -
把獲取對象的方法改為static 通過類名.方法名的方式調用。
輸出結果:
我們把這種類加載的時候就完成對象初始化的創建方式稱為“餓漢模式”。
由于程序在啟動的時候可能需要加載很多的類。單例類,并不一定要在程序啟動的時候用,為了節省計算機資源,加快程序的啟動,可以讓單例類在用到的時候在進行初始化。在編程中延時加載是一個褒義詞。
8.1.2 懶漢模式
- 只聲明這個全局變量,不初始化。
public class SingletonLazy {//定義一個類的成員變量,用static修飾,保證全局唯一private static SingletonLazy instance = null;
}
-
在 獲取單例對象的時候加一個是否為空的判斷,若為空則創建對象。
-
多次獲取對象,打印對象結果。(單線程)
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);}
}
輸出結果:
- 測試在多線程環境中的運行結果
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。所以,線程安全問題得以解決。
給外層加鎖的另一個小問題:
- 當第一個線程進入getInstance 方法時,如果線程還沒有初始化,則獲取鎖進行初始化操作,此時單例對象被第一個線程創建完成。
- 給外層加鎖時,一旦有一個線程獲取到了鎖,那么這個線程就會創建 instance 對象,后面再競爭到鎖的線程就永遠不會進入 if 語句。
- 那么后面的競爭鎖的行為就都是對資源的一種消耗,LOCK和UNLOCK對應的鎖指令是互斥鎖,比較消耗系統資源。
解決問題:
我們在加鎖前再去判斷一下是否需要加鎖。
我們把這種叫做雙重檢查鎖(DCL)
解決內存可見性和指令重排序問題:
DCL的方式必須要學會手寫,面試中如果手寫代碼,必考!!!
面試中使用DCL,工作中使用“餓漢式”
8.2 阻塞隊列
8.2.1 什么是阻塞隊列
阻塞隊列是?種特殊的隊列.也遵守"先進先出"的原則.
阻塞隊列能是?種線程安全的數據結構,并且具有以下特性:
? 當隊列滿的時候,繼續?隊列就會阻塞,直到有其他線程從隊列中取?元素.
? 當隊列空的時候,繼續出隊列也會阻塞,直到有其他線程往隊列中插?元素.
阻塞隊列的?個典型應?場景就是"?產者消費者模型".這是?種?常典型的開發模型.
8.2.2 生產者消費者模型
?產者消費者模式就是通過?個容器來解決?產者和消費者的強耦合問題。
?產者和消費者彼此之間不直接通訊,?通過阻塞隊列來進?通訊,所以?產者?產完數據之后不?等待消費者處理,直接扔給阻塞隊列,消費者不找?產者要數據,?是直接從阻塞隊列?取.
- 阻塞隊列就相當于?個緩沖區,平衡了?產者和消費者的處理能?.(削峰填?)
?如在"秒殺"場景下,服務器同?時刻可能會收到?量的?付請求.如果直接處理這些?付請求,服務器可能扛不住(每個?付請求的處理都需要?較復雜的流程).這個時候就可以把這些請求都放到?個阻塞隊列中,然后再由消費者線程慢慢的來處理每個?付請求.
這樣做可以有效進?"削峰",防?服務器被突然到來的?波請求直接沖垮.
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在生產消息的時候,可以打一個標簽,相當于對消息進行了分類,消費者在獲取消息時,可以根據這個標簽來獲取。
- 阻塞隊列也能使?產者和消費者之間解耦.
?如過年?家??起包餃?.?般都是有明確分?,?如?個?負責搟餃??,其他?負責包.搟餃??的?就是"?產者",包餃?的?就是"消費者".
搟餃??的?不關?包餃?的?是誰(能包就?,?論是??包,借助?具,還是機器包),包餃?的?也不關?搟餃??的?是誰(有餃??就?,?論是?搟?杖搟的,還是拿罐頭瓶搟,還是直接從超市買的).
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
- 掃描線程取出任務執行
- 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 自定義一個線程池
思路:
- 用Runnable描述任務
- 組織管理任務可以使用一個隊列,可以用阻塞隊列去實現,使用阻塞隊列的好處是:當隊列中沒有任務的時候就等待,節省系統資源。
- 提供一個向隊列中添加任務的方法。
- 創建多個線程,掃描隊列中的任務,有任務的時候就取出來執行即可。
寫代碼的時候,要先整理思路,再動手實現。
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:
周末去吃火鍋,火鍋店很火,去的晚了就需要排號。
- 火鍋店里有5張桌子(核心線程數)去了早了,店里沒人就可以直接上桌點菜。
- 越到飯點人越來越多,這時5張桌子都坐滿了,后面來的人就需要排號,最多可以排到20號(當于阻塞隊列,20相當于阻塞隊列的容量)。
- 排隊的人越來越多,已經排到20號了(阻塞隊列已經滿了),在外面加了10張臨時的桌子(臨時線程數,線程池中總的線程數 = 核心線程數 + 臨時線程數)。
- 排號的人就可以在外面的桌子上就餐。
- 時間越來越晚,排隊的人都已經就餐了,外面的桌子慢慢也空下來了,老板說再等30分鐘(臨時線程的存活時間,臨時線程的時間單位),如果再沒人來就把外面的桌子收掉。
- 收掉外面的桌子,店里的5張桌子(最后又回歸到了核心線程數)就可以滿足顧客的就餐要求。
- 中途如果排號滿了20號(阻塞隊列滿了),10張外面的桌子也坐滿了(線程數量達到了線程池的最大個數),老板就不接待客人了(拒絕策略)。
實例2:去銀行辦業務
- 銀行平時只開兩上辦理業務的窗口,相當于線程池的核心線程數。
- 當有新客戶來銀時,看到開放的兩個容口空著,就可以直接去辦理業務。
- 當兩個窗口都有人在辦理業務,后進來的客戶就要去等待區等待。
- 隨著等待的人越來越多,等待區已經滿了,那么銀行就叫來其他的業務員來開放其他三個窗口,一起辦理業務。
- 再來銀行的客戶,就執行拒絕策略。
8.4.6 線程池流程圖
- 添加任務,核心線程從隊列中取任務去執行。
- 核心線程都在工作時,再添加的任務會進入到阻塞隊列。
- 阻塞隊列滿了之后,會創建臨時線程。
- 執行拒絕策略。
8.4.7 拒絕策略
- 直接拒絕
比如公司給分配了一個任務,我說現在我很忙,沒有時間去處理這個任務,你就告訴領導說:你找別人干吧,我沒時間。- 返回給調用者
比如公司給分配了一個任務,我說現在我很忙,沒有時間去處理這個任務,你自己做吧。誰給我分配的任務我就把這個任務返回給誰,保證整個任務有線程執行。- 放棄目前最早等待的任務
比如公司給分配了一個任務,我說現在我很忙,沒有時間去處理這個任務,老板說:最開始給你分的那個活,你可以不干了。
4. 放棄新提交的任務
放棄的任務,以后也找不回來了,所以指定拒絕策略的時候,要關注任務是不是需要必須執行,如果必須執行,就指定“返回調用者”,否則1 3 4 選一個即可,1在拒絕后會拋出異常;3,4在拒絕后不會拋出異常。
- 直接拒絕
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());});}}
}
輸出結果:
- 放棄目前最早的任務
輸出結果:
- 拋棄最新的任務
輸出結果:
- 返回給調用者
輸出結果:
根據不同的業務場景選擇不同的拒絕策略
9. 總結-保證線程安全的思路
- 使用沒有共享資源的模型
- 使用共享資源,只讀不寫的模型
- 不需要寫共享資源的模型
- 使用不可變對象
- 直面線程安全(重點)
- 保證原子性
- 保證順序性
- 保證可見性
10. 對比線程和進程
10.1 線程的優點
- 創建?個新線程的代價要?創建?個新進程?得多
- 與進程之間的切換相?,線程之間的切換需要操作系統做的?作要少很多
- 線程占?的資源要?進程少很多
- 能充分利?多處理器的可并?數量
- 在等待慢速I/O操作結束的同時,程序可執?其他的計算任務
- 計算密集型應?,為了能在多處理器系統上運?,將計算分解到多個線程中實現
- I/O密集型應?,為了提?性能,將I/O操作重疊。線程可以同時等待不同的I/O操作。
10.2 進程與線程的區別
- 進程是系統進?資源分配和調度的?個獨?單位,線程是程序執?的最?單位。
- 進程有??的內存地址空間,線程只獨享指令流執?的必要資源,如寄存器和棧。
- 由于同?進程的各線程間共享內存和?件資源,可以不通過內核進?直接通信。
- 線程的創建、切換及終?效率更?。
11. wait() 和 sleep()的區別
- 共同點,都會讓線程阻塞一會兒
- 從實現使用上來說是兩種不同的方式wait是Object類的方法,和鎖相關,配合synchronized一起使用,調用wait之后會釋放鎖sleep是Thread類的方法,與鎖無關.
- wait可以通過指定超時時間和通過notify方法喚醒,喚醒之后會重新競爭鎖資源sleep只能通過超時時間喚醒