day07【線程池、Lambda表達式】
主要內容
-
等待與喚醒案例
-
線程池
-
Lambda表達式
教學目標
-[ ] 能夠理解線程通信概念-[ ] 能夠理解等待喚醒機制-[ ] 能夠描述Java中線程池運行原理-[ ] 能夠理解函數式編程相對於面向對象的優點-[ ] 能夠掌握Lambda表達式的標准格式-[ ] 能夠使用Lambda標准格式使用Runnable與Comparator接口-[ ] 能夠掌握Lambda表達式的省略格式與規則-[ ] 能夠使用Lambda省略格式使用Runnable與Comparator接口-[ ] 能夠通過Lambda的標准格式使用自定義的接口(有且僅有一個抽象方法)-[ ] 能夠通過Lambda的省略格式使用自定義的接口(有且僅有一個抽象方法)-[ ] 能夠明確Lambda的兩項使用前提
第一章 等待喚醒機制
1.1 線程間通信
概念:多個線程在處理同一個資源,但是處理的動作(線程的任務)卻不相同。
比如:線程A用來生成包子的,線程B用來吃包子的,包子可以理解為同一資源,線程A與線程B處理的動作,一個是生產,一個是消費,那么線程A與線程B之間就存在線程通信問題。
為什么要處理線程間通信:
多個線程並發執行時, 在默認情況下CPU是隨機切換線程的,當我們需要多個線程來共同完成一件任務,並且我們希望他們有規律的執行, 那么多線程之間需要一些協調通信,以此來幫我們達到多線程共同操作一份數據。
如何保證線程間通信有效利用資源:
多個線程在處理同一個資源,並且任務不同時,需要線程通信來幫助解決線程之間對同一個變量的使用或操作。 就是多個線程在操作同一份數據時, 避免對同一共享變量的爭奪。也就是我們需要通過一定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制。
1.2 等待喚醒機制
什么是等待喚醒機制
這是多個線程間的一種協作機制。談到線程我們經常想到的是線程間的競爭(race),比如去爭奪鎖,但這並不是故事的全部,線程間也會有協作機制。就好比在公司里你和你的同事們,你們可能存在在晉升時的競爭,但更多時候你們更多是一起合作以完成某些任務。
就是在一個線程進行了規定操作后,就進入等待狀態(wait()), 等待其他線程執行完他們的指定代碼過后 再將其喚醒(notify());在有多個線程進行等待時, 如果需要,可以使用 notifyAll()來喚醒所有的等待線程。
wait/notify 就是線程間的一種協作機制。
等待喚醒中的方法
等待喚醒機制就是用於解決線程間通信的問題的,使用到的3個方法的含義如下:
-
wait:線程不再活動,不再參與調度,進入 wait set 中,因此不會浪費 CPU 資源,也不會去競爭鎖了,這時的線程狀態即是 WAITING。它還要等着別的線程執行一個特別的動作,也即是“通知(notify)”在這個對象上等待的線程從wait set 中釋放出來,重新進入到調度隊列(ready queue)中
-
notify:則選取所通知對象的 wait set 中的一個線程釋放;例如,餐館有空位置后,等候就餐最久的顧客最先入座。
-
notifyAll:則釋放所通知對象的 wait set 上的全部線程。
注意:
哪怕只通知了一個等待的線程,被通知線程也不能立即恢復執行,因為它當初中斷的地方是在同步塊內,而此刻它已經不持有鎖,所以她需要再次嘗試去獲取鎖(很可能面臨其它線程的競爭),成功后才能在當初調用 wait 方法之后的地方恢復執行。
總結如下:
如果能獲取鎖,線程就從 WAITING 狀態變成 RUNNABLE 狀態;
否則,從 wait set 出來,又進入 entry set,線程就從 WAITING 狀態又變成 BLOCKED 狀態
調用wait和notify方法需要注意的細節
-
wait方法與notify方法必須要由同一個鎖對象調用。因為:對應的鎖對象可以通過notify喚醒使用同一個鎖對象調用的wait方法后的線程。
-
wait方法與notify方法是屬於Object類的方法的。因為:鎖對象可以是任意對象,而任意對象的所屬類都是繼承了Object類的。
-
wait方法與notify方法必須要在同步代碼塊或者是同步函數中使用。因為:必須要通過鎖對象調用這2個方法。
1.3 生產者與消費者問題
等待喚醒機制其實就是經典的“生產者與消費者”的問題。
就拿生產包子消費包子來說等待喚醒機制如何有效利用資源:
包子鋪線程生產包子,吃貨線程消費包子。當包子沒有時(包子狀態為false),吃貨線程等待,包子鋪線程生產包子(即包子狀態為true),並通知吃貨線程(解除吃貨的等待狀態),因為已經有包子了,那么包子鋪線程進入等待狀態。接下來,吃貨線程能否進一步執行則取決於鎖的獲取情況。如果吃貨獲取到鎖,那么就執行吃包子動作,包子吃完(包子狀態為false),並通知包子鋪線程(解除包子鋪的等待狀態),吃貨線程進入等待。包子鋪線程能否進一步執行則取決於鎖的獲取情況。
代碼演示:
包子資源類:
public class BaoZi { String pier ; String xianer ; boolean flag = false ;//包子資源 是否存在 包子資源狀態 }
吃貨線程類:
public class ChiHuo extends Thread{ private BaoZi bz; public ChiHuo(String name,BaoZi bz){ super(name); this.bz = bz; } @Override public void run() { while(true){ synchronized (bz){ if(bz.flag == false){//沒包子 try { bz.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("吃貨正在吃"+bz.pier+bz.xianer+"包子"); bz.flag = false; bz.notify(); } } } }
包子鋪線程類:
public class BaoZiPu extends Thread { private BaoZi bz; public BaoZiPu(String name,BaoZi bz){ super(name); this.bz = bz; } @Override public void run() { int count = 0; //造包子 while(true){ //同步 synchronized (bz){ if(bz.flag == true){//包子資源 存在 try { bz.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 沒有包子 造包子 System.out.println("包子鋪開始做包子"); if(count%2 == 0){ // 冰皮 五仁 bz.pier = "冰皮"; bz.xianer = "五仁"; }else{ // 薄皮 牛肉大蔥 bz.pier = "薄皮"; bz.xianer = "牛肉大蔥"; } count++; bz.flag=true; System.out.println("包子造好了:"+bz.pier+bz.xianer); System.out.println("吃貨來吃吧"); //喚醒等待線程 (吃貨) bz.notify(); } } } }
測試類:
public class Demo { public static void main(String[] args) { //等待喚醒案例 BaoZi bz = new BaoZi(); ChiHuo ch = new ChiHuo("吃貨",bz); BaoZiPu bzp = new BaoZiPu("包子鋪",bz); ch.start(); bzp.start(); } }
執行效果:
包子鋪開始做包子 包子造好了:冰皮五仁 吃貨來吃吧 吃貨正在吃冰皮五仁包子 包子鋪開始做包子 包子造好了:薄皮牛肉大蔥 吃貨來吃吧 吃貨正在吃薄皮牛肉大蔥包子 包子鋪開始做包子 包子造好了:冰皮五仁 吃貨來吃吧 吃貨正在吃冰皮五仁包子
第二章 線程池
2.1 線程池思想概述
我們使用線程的時候就去創建一個線程,這樣實現起來非常簡便,但是就會有一個問題:
如果並發的線程數量很多,並且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因為頻繁創建線程和銷毀線程需要時間。
那么有沒有一種辦法使得線程可以復用,就是執行完一個任務,並不被銷毀,而是可以繼續執行其他的任務?
在Java中可以通過線程池來達到這樣的效果。今天我們就來詳細講解一下Java的線程池。
2.2 線程池概念
-
線程池:其實就是一個容納多個線程的容器,其中的線程可以反復使用,省去了頻繁創建線程對象的操作,無需反復創建線程而消耗過多資源。
由於線程池中有很多操作都是與優化資源相關的,我們在這里就不多贅述。我們通過一張圖來了解線程池的工作原理:
合理利用線程池能夠帶來三個好處:
-
降低資源消耗。減少了創建和銷毀線程的次數,每個工作線程都可以被重復利用,可執行多個任務。
-
提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
-
提高線程的可管理性。可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因為消耗過多的內存,而把服務器累趴下(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,最后死機)。
2.3 線程池的使用
Java里面線程池的頂級接口是java.util.concurrent.Executor
,但是嚴格意義上講Executor
並不是一個線程池,而只是一個執行線程的工具。真正的線程池接口是java.util.concurrent.ExecutorService
。
要配置一個線程池是比較復雜的,尤其是對於線程池的原理不是很清楚的情況下,很有可能配置的線程池不是較優的,因此在java.util.concurrent.Executors
線程工廠類里面提供了一些靜態工廠,生成一些常用的線程池。官方建議使用Executors工程類來創建線程池對象。
Executors類中有個創建線程池的方法如下:
-
public static ExecutorService newFixedThreadPool(int nThreads)
:返回線程池對象。(創建的是有界線程池,也就是池中的線程個數可以指定最大數量)
獲取到了一個線程池ExecutorService 對象,那么怎么使用呢,在這里定義了一個使用線程池對象的方法如下:
-
public Future<?> submit(Runnable task)
:獲取線程池中的某一個線程對象,並執行Future接口:用來記錄線程任務執行完畢后產生的結果。線程池創建與使用。
使用線程池中線程對象的步驟:
-
創建線程池對象。
-
創建Runnable接口子類對象。(task)
-
提交Runnable接口子類對象。(take task)
-
關閉線程池(一般不做)。
Runnable實現類代碼:
public class MyRunnable implements Runnable { @Override public void run() { System.out.println("我要一個教練"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("教練來了: " + Thread.currentThread().getName()); System.out.println("教我游泳,交完后,教練回到了游泳池"); } }
線程池測試類:
public class ThreadPoolDemo { public static void main(String[] args) { // 創建線程池對象 ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象 // 創建Runnable實例對象 MyRunnable r = new MyRunnable(); //自己創建線程對象的方式 // Thread t = new Thread(r); // t.start(); ---> 調用MyRunnable中的run() // 從線程池中獲取線程對象,然后調用MyRunnable中的run() service.submit(r); // 再獲取個線程對象,調用MyRunnable中的run() service.submit(r); service.submit(r); // 注意:submit方法調用結束后,程序並不終止,是因為線程池控制了線程的關閉。 // 將使用完的線程又歸還到了線程池中 // 關閉線程池 //service.shutdown(); } }
第三章 Lambda表達式
3.1 函數式編程思想概述
在數學中,函數就是有輸入量、輸出量的一套計算方案,也就是“拿什么東西做什么事情”。相對而言,面向對象過分強調“必須通過對象的形式來做事情”,而函數式思想則盡量忽略面向對象的復雜語法——強調做什么,而不是以什么形式做。
面向對象的思想:
做一件事情,找一個能解決這個事情的對象,調用對象的方法,完成事情.
函數式編程思想:
只要能獲取到結果,誰去做的,怎么做的都不重要,重視的是結果,不重視過程
3.2 冗余的Runnable代碼
傳統寫法
當需要啟動一個線程去完成任務時,通常會通過java.lang.Runnable
接口來定義任務內容,並使用java.lang.Thread
類來啟動該線程。代碼如下:
public class Demo01Runnable { public static void main(String[] args) { // 匿名內部類 Runnable task = new Runnable() { @Override public void run() { // 覆蓋重寫抽象方法 System.out.println("多線程任務執行!"); } }; new Thread(task).start(); // 啟動線程 } }
本着“一切皆對象”的思想,這種做法是無可厚非的:首先創建一個Runnable
接口的匿名內部類對象來指定任務內容,再將其交給一個線程來啟動。
代碼分析
對於Runnable
的匿名內部類用法,可以分析出幾點內容:
-
Thread
類需要Runnable
接口作為參數,其中的抽象run
方法是用來指定線程任務內容的核心; -
為了指定
run
的方法體,不得不需要Runnable
接口的實現類; -
為了省去定義一個
RunnableImpl
實現類的麻煩,不得不使用匿名內部類; -
必須覆蓋重寫抽象
run
方法,所以方法名稱、方法參數、方法返回值不得不再寫一遍,且不能寫錯; -
而實際上,似乎只有方法體才是關鍵所在。
3.3 編程思想轉換
做什么,而不是怎么做
我們真的希望創建一個匿名內部類對象嗎?不。我們只是為了做這件事情而不得不創建一個對象。我們真正希望做的事情是:將run
方法體內的代碼傳遞給Thread
類知曉。
傳遞一段代碼——這才是我們真正的目的。而創建對象只是受限於面向對象語法而不得不采取的一種手段方式。那,有沒有更加簡單的辦法?如果我們將關注點從“怎么做”回歸到“做什么”的本質上,就會發現只要能夠更好地達到目的,過程與形式其實並不重要。
生活舉例
當我們需要從北京到上海時,可以選擇高鐵、汽車、騎行或是徒步。我們的真正目的是到達上海,而如何才能到達上海的形式並不重要,所以我們一直在探索有沒有比高鐵更好的方式——搭乘飛機。
而現在這種飛機(甚至是飛船)已經誕生:2014年3月Oracle所發布的Java 8(JDK 1.8)中,加入了Lambda表達式的重量級新特性,為我們打開了新世界的大門。
3.4 體驗Lambda的更優寫法
借助Java 8的全新語法,上述Runnable
接口的匿名內部類寫法可以通過更簡單的Lambda表達式達到等效:
public class Demo02LambdaRunnable { public static void main(String[] args) { new Thread(() -> System.out.println("多線程任務執行!")).start(); // 啟動線程 } }
這段代碼和剛才的執行效果是完全一樣的,可以在1.8或更高的編譯級別下通過。從代碼的語義中可以看出:我們啟動了一個線程,而線程任務的內容以一種更加簡潔的形式被指定。
不再有“不得不創建接口對象”的束縛,不再有“抽象方法覆蓋重寫”的負擔,就是這么簡單!
3.5 回顧匿名內部類
Lambda是怎樣擊敗面向對象的?在上例中,核心代碼其實只是如下所示的內容:
() -> System.out.println("多線程任務執行!")
為了理解Lambda的語義,我們需要從傳統的代碼起步。
使用實現類
要啟動一個線程,需要創建一個Thread
類的對象並調用start
方法。而為了指定線程執行的內容,需要調用Thread
類的構造方法:
-
public Thread(Runnable target)
為了獲取Runnable
接口的實現對象,可以為該接口定義一個實現類RunnableImpl
:
public class RunnableImpl implements Runnable { @Override public void run() { System.out.println("多線程任務執行!"); } }
然后創建該實現類的對象作為Thread
類的構造參數:
public class Demo03ThreadInitParam { public static void main(String[] args) { Runnable task = new RunnableImpl(); new Thread(task).start(); } }
使用匿名內部類
這個RunnableImpl
類只是為了實現Runnable
接口而存在的,而且僅被使用了唯一一次,所以使用匿名內部類的語法即可省去該類的單獨定義,即匿名內部類:
public class Demo04ThreadNameless { public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println("多線程任務執行!"); } }).start(); } }
匿名內部類的好處與弊端
一方面,匿名內部類可以幫我們省去實現類的定義;另一方面,匿名內部類的語法——確實太復雜了!
語義分析
仔細分析該代碼中的語義,Runnable
接口只有一個run
方法的定義:
-
public abstract void run();
即制定了一種做事情的方案(其實就是一個函數):
-
無參數:不需要任何條件即可執行該方案。
-
無返回值:該方案不產生任何結果。
-
代碼塊(方法體):該方案的具體執行步驟。
同樣的語義體現在Lambda
語法中,要更加簡單:
() -> System.out.println("多線程任務執行!")
-
前面的一對小括號即
run
方法的參數(無),代表不需要任何條件; -
中間的一個箭頭代表將前面的參數傳遞給后面的代碼;
-
后面的輸出語句即業務邏輯代碼。
3.6 Lambda標准格式
Lambda省去面向對象的條條框框,格式由3個部分組成:
-
一些參數
-
一個箭頭
-
一段代碼
Lambda表達式的標准格式為:
(參數類型 參數名稱) -> { 代碼語句 }
格式說明:
-
小括號內的語法與傳統方法參數列表一致:無參數則留空;多個參數則用逗號分隔。
-
->
是新引入的語法格式,代表指向動作。 -
大括號內的語法與傳統方法體要求基本一致。
3.7 練習:使用Lambda標准格式(無參無返回)
題目
給定一個廚子Cook
接口,內含唯一的抽象方法makeFood
,且無參數、無返回值。如下:
public interface Cook { void makeFood(); }
在下面的代碼中,請使用Lambda的標准格式調用invokeCook
方法,打印輸出“吃飯啦!”字樣:
public class Demo05InvokeCook { public static void main(String[] args) { // TODO 請在此使用Lambda【標准格式】調用invokeCook方法 } private static void invokeCook(Cook cook) { cook.makeFood(); } }
解答
public static void main(String[] args) { invokeCook(() -> { System.out.println("吃飯啦!"); }); }
備注:小括號代表
Cook
接口makeFood
抽象方法的參數為空,大括號代表makeFood
的方法體。
3.8 Lambda的參數和返回值
需求: 使用數組存儲多個Person對象 對數組中的Person對象使用Arrays的sort方法通過年齡進行升序排序
下面舉例演示java.util.Comparator<T>
接口的使用場景代碼,其中的抽象方法定義為:
-
public abstract int compare(T o1, T o2);
當需要對一個對象數組進行排序時,Arrays.sort
方法需要一個Comparator
接口實例來指定排序的規則。假設有一個Person
類,含有String name
和int age
兩個成員變量:
public class Person { private String name; private int age; // 省略構造器、toString方法與Getter Setter }
傳統寫法
如果使用傳統的代碼對Person[]
數組進行排序,寫法如下:
import java.util.Arrays; import java.util.Comparator; public class Demo06Comparator { public static void main(String[] args) { // 本來年齡亂序的對象數組 Person[] array = { new Person("古力娜扎", 19), new Person("迪麗熱巴", 18), new Person("馬爾扎哈", 20) }; // 匿名內部類 Comparator<Person> comp = new Comparator<Person>() { @Override public int compare(Person o1, Person o2) { return o1.getAge() - o2.getAge(); } }; Arrays.sort(array, comp); // 第二個參數為排序規則,即Comparator接口實例 for (Person person : array) { System.out.println(person); } } }
這種做法在面向對象的思想中,似乎也是“理所當然”的。其中Comparator
接口的實例(使用了匿名內部類)代表了“按照年齡從小到大”的排序規則。
代碼分析
下面我們來搞清楚上述代碼真正要做什么事情。
-
為了排序,
Arrays.sort
方法需要排序規則,即Comparator
接口的實例,抽象方法compare
是關鍵; -
為了指定
compare
的方法體,不得不需要Comparator
接口的實現類; -
為了省去定義一個
ComparatorImpl
實現類的麻煩,不得不使用匿名內部類; -
必須覆蓋重寫抽象
compare
方法,所以方法名稱、方法參數、方法返回值不得不再寫一遍,且不能寫錯; -
實際上,只有參數和方法體才是關鍵。
Lambda寫法
import java.util.Arrays; public class Demo07ComparatorLambda { public static void main(String[] args) { Person[] array = { new Person("古力娜扎", 19), new Person("迪麗熱巴", 18), new Person("馬爾扎哈", 20) }; Arrays.sort(array, (Person a, Person b) -> { return a.getAge() - b.getAge(); }); for (Person person : array) { System.out.println(person); } } }
3.9 練習:使用Lambda標准格式(有參有返回)
題目
給定一個計算器Calculator
接口,內含抽象方法calc
可以將兩個int數字相加得到和值:
public interface Calculator { int calc(int a, int b); }
在下面的代碼中,請使用Lambda的標准格式調用invokeCalc
方法,完成120和130的相加計算:
public class Demo08InvokeCalc { public static void main(String[] args) { // TODO 請在此使用Lambda【標准格式】調用invokeCalc方法來計算120+130的結果ß } private static void invokeCalc(int a, int b, Calculator calculator) { int result = calculator.calc(a, b); System.out.println("結果是:" + result); } }
解答
public static void main(String[] args) { invokeCalc(120, 130, (int a, int b) -> { return a + b; }); }
備注:小括號代表
Calculator
接口calc
抽象方法的參數,大括號代表calc
的方法體。
3.10 Lambda省略格式
可推導即可省略
Lambda強調的是“做什么”而不是“怎么做”,所以凡是可以根據上下文推導得知的信息,都可以省略。例如上例還可以使用Lambda的省略寫法:
public static void main(String[] args) { invokeCalc(120, 130, (a, b) -> a + b); }
省略規則
在Lambda標准格式的基礎上,使用省略寫法的規則為:
-
小括號內參數的類型可以省略;
-
如果小括號內有且僅有一個參,則小括號可以省略;
-
如果大括號內有且僅有一個語句,則無論是否有返回值,都可以省略大括號、return關鍵字及語句分號。
備注:掌握這些省略規則后,請對應地回顧本章開頭的多線程案例。
3.11 練習:使用Lambda省略格式
題目
仍然使用前文含有唯一makeFood
抽象方法的廚子Cook
接口,在下面的代碼中,請使用Lambda的省略格式調用invokeCook
方法,打印輸出“吃飯啦!”字樣:
public class Demo09InvokeCook { public static void main(String[] args) { // TODO 請在此使用Lambda【省略格式】調用invokeCook方法 } private static void invokeCook(Cook cook) { cook.makeFood(); } }
解答
public static void main(String[] args) { invokeCook(() -> System.out.println("吃飯啦!")); }
3.12 Lambda的使用前提
Lambda的語法非常簡潔,完全沒有面向對象復雜的束縛。但是使用時有幾個問題需要特別注意:
-
使用Lambda必須具有接口,且要求接口中有且僅有一個抽象方法。無論是JDK內置的
Runnable
、Comparator
接口還是自定義的接口,只有當接口中的抽象方法存在且唯一時,才可以使用Lambda。 -
使用Lambda必須具有上下文推斷。也就是方法的參數或局部變量類型必須為Lambda對應的接口類型,才能使用Lambda作為該接口的實例。
備注:有且僅有一個抽象方法的接口,稱為“函數式接口”。