概念:多個線程在處理同一個資源,可是處理的動做(線程的任務)卻不相同。java
好比:線程A用來生成包子的,線程B用來吃包子的,包子能夠理解爲同一資源,線程A與線程B處理的動做,一個是生產,一個是消費,那麼線程A與線程B之間就存在線程通訊問題。編程
爲何要處理線程間通訊:數組
多個線程併發執行時, 在默認狀況下CPU是隨機切換線程的,當咱們須要多個線程來共同完成一件任務,而且咱們但願他們有規律的執行, 那麼多線程之間須要一些協調通訊,以此來幫咱們達到多線程共同操做一份數據。服務器
如何保證線程間通訊有效利用資源:多線程
多個線程在處理同一個資源,而且任務不一樣時,須要線程通訊來幫助解決線程之間對同一個變量的使用或操做。 就是多個線程在操做同一份數據時, 避免對同一共享變量的爭奪。也就是咱們須要經過必定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制。併發
什麼是等待喚醒機制less
這是多個線程間的一種協做機制。談到線程咱們常常想到的是線程間的競爭(race),好比去爭奪鎖,但這並非故事的所有,線程間也會有協做機制。就比如在公司裏你和你的同事們,大家可能存在在晉升時的競爭,但更多時候大家更可能是一塊兒合做以完成某些任務。ide
就是在一個線程進行了規定操做後,就進入等待狀態(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個方法。
等待喚醒機制其實就是經典的「生產者與消費者」的問題。
就拿生產包子消費包子來講等待喚醒機制如何有效利用資源:
包子鋪線程生產包子,吃貨線程消費包子。當包子沒有時(包子狀態爲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;
}
包子鋪線程類:
public class BaoZiPu extends Thread {
private BaoZi bz;
public BaoZiPu(String name,BaoZi bz){
super(name);
this.bz = bz;
}
測試類:
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();
}
}
執行效果:
包子鋪開始作包子
包子造好了:冰皮五仁
吃貨來吃吧
吃貨正在吃冰皮五仁包子
包子鋪開始作包子
包子造好了:薄皮牛肉大蔥
吃貨來吃吧
吃貨正在吃薄皮牛肉大蔥包子
包子鋪開始作包子
包子造好了:冰皮五仁
吃貨來吃吧
吃貨正在吃冰皮五仁包子
咱們使用線程的時候就去建立一個線程,這樣實現起來很是簡便,可是就會有一個問題:
若是併發的線程數量不少,而且每一個線程都是執行一個時間很短的任務就結束了,這樣頻繁建立線程就會大大下降系統的效率,由於頻繁建立線程和銷燬線程須要時間。
那麼有沒有一種辦法使得線程能夠複用,就是執行完一個任務,並不被銷燬,而是能夠繼續執行其餘的任務?
在Java中能夠經過線程池來達到這樣的效果。今天咱們就來詳細講解一下Java的線程池。
線程池:其實就是一個容納多個線程的容器,其中的線程能夠反覆使用,省去了頻繁建立線程對象的操做,無需反覆建立線程而消耗過多資源。
因爲線程池中有不少操做都是與優化資源相關的,咱們在這裏就很少贅述。咱們經過一張圖來了解線程池的工做原理:
合理利用線程池可以帶來三個好處:
下降資源消耗。減小了建立和銷燬線程的次數,每一個工做線程均可以被重複利用,可執行多個任務。
提升響應速度。當任務到達時,任務能夠不須要的等到線程建立就能當即執行。
提升線程的可管理性。能夠根據系統的承受能力,調整線程池中工做線線程的數目,防止由於消耗過多的內存,而把服務器累趴下(每一個線程須要大約1MB內存,線程開的越多,消耗的內存也就越大,最後死機)。
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 {
線程池測試類:
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();
}
}
在數學中,函數就是有輸入量、輸出量的一套計算方案,也就是「拿什麼東西作什麼事情」。相對而言,面向對象過度強調「必須經過對象的形式來作事情」,而函數式思想則儘可能忽略面向對象的複雜語法——強調作什麼,而不是以什麼形式作。
面向對象的思想:
作一件事情,找一個能解決這個事情的對象,調用對象的方法,完成事情.
函數式編程思想:
只要能獲取到結果,誰去作的,怎麼作的都不重要,重視的是結果,不重視過程
當須要啓動一個線程去完成任務時,一般會經過java.lang.Runnable
接口來定義任務內容,並使用java.lang.Thread
類來啓動該線程。代碼以下:
public class Demo01Runnable {
public static void main(String[] args) {
// 匿名內部類
Runnable task = new Runnable() {
本着「一切皆對象」的思想,這種作法是無可厚非的:首先建立一個Runnable
接口的匿名內部類對象來指定任務內容,再將其交給一個線程來啓動。
對於Runnable
的匿名內部類用法,能夠分析出幾點內容:
Thread
類須要Runnable
接口做爲參數,其中的抽象run
方法是用來指定線程任務內容的核心;
爲了指定run
的方法體,不得不須要Runnable
接口的實現類;
爲了省去定義一個RunnableImpl
實現類的麻煩,不得不使用匿名內部類;
必須覆蓋重寫抽象run
方法,因此方法名稱、方法參數、方法返回值不得不再寫一遍,且不能寫錯;
而實際上,彷佛只有方法體纔是關鍵所在。
咱們真的但願建立一個匿名內部類對象嗎?不。咱們只是爲了作這件事情而不得不建立一個對象。咱們真正但願作的事情是:將run
方法體內的代碼傳遞給Thread
類知曉。
傳遞一段代碼——這纔是咱們真正的目的。而建立對象只是受限於面向對象語法而不得不採起的一種手段方式。那,有沒有更加簡單的辦法?若是咱們將關注點從「怎麼作」迴歸到「作什麼」的本質上,就會發現只要可以更好地達到目的,過程與形式其實並不重要。
當咱們須要從北京到上海時,能夠選擇高鐵、汽車、騎行或是徒步。咱們的真正目的是到達上海,而如何才能到達上海的形式並不重要,因此咱們一直在探索有沒有比高鐵更好的方式——搭乘飛機。
而如今這種飛機(甚至是飛船)已經誕生:2014年3月Oracle所發佈的Java 8(JDK 1.8)中,加入了Lambda表達式的重量級新特性,爲咱們打開了新世界的大門。
藉助Java 8的全新語法,上述Runnable
接口的匿名內部類寫法能夠經過更簡單的Lambda表達式達到等效:
public class Demo02LambdaRunnable {
public static void main(String[] args) {
new Thread(() -> System.out.println("多線程任務執行!")).start(); // 啓動線程
}
}
這段代碼和剛纔的執行效果是徹底同樣的,能夠在1.8或更高的編譯級別下經過。從代碼的語義中能夠看出:咱們啓動了一個線程,而線程任務的內容以一種更加簡潔的形式被指定。
再也不有「不得不建立接口對象」的束縛,再也不有「抽象方法覆蓋重寫」的負擔,就是這麼簡單!
Lambda是怎樣擊敗面向對象的?在上例中,核心代碼其實只是以下所示的內容:
() -> System.out.println("多線程任務執行!")
爲了理解Lambda的語義,咱們須要從傳統的代碼起步。
要啓動一個線程,須要建立一個Thread
類的對象並調用start
方法。而爲了指定線程執行的內容,須要調用Thread
類的構造方法:
public Thread(Runnable target)
爲了獲取Runnable
接口的實現對象,能夠爲該接口定義一個實現類RunnableImpl
:
public class RunnableImpl implements Runnable {
而後建立該實現類的對象做爲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() {
一方面,匿名內部類能夠幫咱們省去實現類的定義;另外一方面,匿名內部類的語法——確實太複雜了!
仔細分析該代碼中的語義,Runnable
接口只有一個run
方法的定義:
public abstract void run();
即制定了一種作事情的方案(其實就是一個函數):
無參數:不須要任何條件便可執行該方案。
無返回值:該方案不產生任何結果。
代碼塊(方法體):該方案的具體執行步驟。
一樣的語義體如今Lambda
語法中,要更加簡單:
() -> System.out.println("多線程任務執行!")
前面的一對小括號即run
方法的參數(無),表明不須要任何條件;
中間的一個箭頭表明將前面的參數傳遞給後面的代碼;
後面的輸出語句即業務邏輯代碼。
Lambda省去面向對象的條條框框,格式由3個部分組成:
一些參數
一個箭頭
一段代碼
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
的方法體。
需求:
使用數組存儲多個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>() {
這種作法在面向對象的思想中,彷佛也是「理所固然」的。其中Comparator
接口的實例(使用了匿名內部類)表明了「按照年齡從小到大」的排序規則。
下面咱們來搞清楚上述代碼真正要作什麼事情。
爲了排序,Arrays.sort
方法須要排序規則,即Comparator
接口的實例,抽象方法compare
是關鍵;
爲了指定compare
的方法體,不得不須要Comparator
接口的實現類;
爲了省去定義一個ComparatorImpl
實現類的麻煩,不得不使用匿名內部類;
必須覆蓋重寫抽象compare
方法,因此方法名稱、方法參數、方法返回值不得不再寫一遍,且不能寫錯;
實際上,只有參數和方法體纔是關鍵。
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);
}
}
}
給定一個計算器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
的方法體。
Lambda強調的是「作什麼」而不是「怎麼作」,因此凡是能夠根據上下文推導得知的信息,均可以省略。例如上例還可使用Lambda的省略寫法:
public static void main(String[] args) {
invokeCalc(120, 130, (a, b) -> a + b);
}
在Lambda標準格式的基礎上,使用省略寫法的規則爲:
小括號內參數的類型能夠省略;
若是小括號內有且僅有一個參,則小括號能夠省略;
若是大括號內有且僅有一個語句,則不管是否有返回值,均可以省略大括號、return關鍵字及語句分號。
備註:掌握這些省略規則後,請對應地回顧本章開頭的多線程案例。
仍然使用前文含有惟一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("吃飯啦!"));
}
Lambda的語法很是簡潔,徹底沒有面向對象複雜的束縛。可是使用時有幾個問題須要特別注意:
使用Lambda必須具備接口,且要求接口中有且僅有一個抽象方法。 不管是JDK內置的Runnable
、Comparator
接口仍是自定義的接口,只有當接口中的抽象方法存在且惟一時,纔可使用Lambda。
使用Lambda必須具備上下文推斷。 也就是方法的參數或局部變量類型必須爲Lambda對應的接口類型,才能使用Lambda做爲該接口的實例。
備註:有且僅有一個抽象方法的接口,稱爲「函數式接口」。