java day18【線程池、Lambda表達式】

第一章 等待喚醒機制

1.1 線程間通訊

概念:多個線程在處理同一個資源,可是處理的動做(線程的任務)卻不相同。java

好比:線程A用來生成包子的,線程B用來吃包子的,包子能夠理解爲同一資源,線程A與線程B處理的動做,一個是生產,一個是消費,那麼線程A與線程B之間就存在線程通訊問題。編程

 

爲何要處理線程間通訊:數組

多個線程併發執行時, 在默認狀況下CPU是隨機切換線程的,當咱們須要多個線程來共同完成一件任務,而且咱們但願他們有規律的執行, 那麼多線程之間須要一些協調通訊,以此來幫咱們達到多線程共同操做一份數據。服務器

如何保證線程間通訊有效利用資源:多線程

多個線程在處理同一個資源,而且任務不一樣時,須要線程通訊來幫助解決線程之間對同一個變量的使用或操做。 就是多個線程在操做同一份數據時, 避免對同一共享變量的爭奪。也就是咱們須要經過必定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制。併發

1.2 等待喚醒機制

什麼是等待喚醒機制less

這是多個線程間的一種協做機制。談到線程咱們常常想到的是線程間的競爭(race),好比去爭奪鎖,但這並非故事的所有,線程間也會有協做機制。就比如在公司裏你和你的同事們,大家可能存在在晉升時的競爭,但更多時候大家更可能是一塊兒合做以完成某些任務。ide

就是在一個線程進行了規定操做後,就進入等待狀態(wait()), 等待其餘線程執行完他們的指定代碼事後 再將其喚醒(notify());在有多個線程進行等待時, 若是須要,可使用 notifyAll()來喚醒全部的等待線程。函數式編程

wait/notify 就是線程間的一種協做機制。函數

等待喚醒中的方法

等待喚醒機制就是用於解決線程間通訊的問題的,使用到的3個方法的含義以下:

  1. wait:線程再也不活動,再也不參與調度,進入 wait set 中,所以不會浪費 CPU 資源,也不會去競爭鎖了,這時的線程狀態便是 WAITING。它還要等着別的線程執行一個特別的動做,也便是「通知(notify)」在這個對象上等待的線程從wait set 中釋放出來,從新進入到調度隊列(ready queue)中

  2. notify:則選取所通知對象的 wait set 中的一個線程釋放;例如,餐館有空位置後,等候就餐最久的顧客最早入座。

  3. notifyAll:則釋放所通知對象的 wait set 上的所有線程。

注意:

哪怕只通知了一個等待的線程,被通知線程也不能當即恢復執行,由於它當初中斷的地方是在同步塊內,而此刻它已經不持有鎖,因此她須要再次嘗試去獲取鎖(極可能面臨其它線程的競爭),成功後才能在當初調用 wait 方法以後的地方恢復執行。

總結以下:

  • 若是能獲取鎖,線程就從 WAITING 狀態變成 RUNNABLE 狀態;

  • 不然,從 wait set 出來,又進入 entry set,線程就從 WAITING 狀態又變成 BLOCKED 狀態

調用wait和notify方法須要注意的細節

  1. wait方法與notify方法必需要由同一個鎖對象調用。由於:對應的鎖對象能夠經過notify喚醒使用同一個鎖對象調用的wait方法後的線程。

  2. wait方法與notify方法是屬於Object類的方法的。由於:鎖對象能夠是任意對象,而任意對象的所屬類都是繼承了Object類的。

  3. 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 線程池概念

  • 線程池:其實就是一個容納多個線程的容器,其中的線程能夠反覆使用,省去了頻繁建立線程對象的操做,無需反覆建立線程而消耗過多資源。

因爲線程池中有不少操做都是與優化資源相關的,咱們在這裏就很少贅述。咱們經過一張圖來了解線程池的工做原理:

 

合理利用線程池可以帶來三個好處:

  1. 下降資源消耗。減小了建立和銷燬線程的次數,每一個工做線程均可以被重複利用,可執行多個任務。

  2. 提升響應速度。當任務到達時,任務能夠不須要的等到線程建立就能當即執行。

  3. 提升線程的可管理性。能夠根據系統的承受能力,調整線程池中工做線線程的數目,防止由於消耗過多的內存,而把服務器累趴下(每一個線程須要大約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接口:用來記錄線程任務執行完畢後產生的結果。線程池建立與使用。

使用線程池中線程對象的步驟:

  1. 建立線程池對象。

  2. 建立Runnable接口子類對象。(task)

  3. 提交Runnable接口子類對象。(take task)

  4. 關閉線程池(通常不作)。

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 nameint 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標準格式的基礎上,使用省略寫法的規則爲:

  1. 小括號內參數的類型能夠省略;

  2. 若是小括號內有且僅有一個參,則小括號能夠省略;

  3. 若是大括號內有且僅有一個語句,則不管是否有返回值,均可以省略大括號、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的語法很是簡潔,徹底沒有面向對象複雜的束縛。可是使用時有幾個問題須要特別注意:

  1. 使用Lambda必須具備接口,且要求接口中有且僅有一個抽象方法 不管是JDK內置的RunnableComparator接口仍是自定義的接口,只有當接口中的抽象方法存在且惟一時,纔可使用Lambda。

  2. 使用Lambda必須具備上下文推斷 也就是方法的參數或局部變量類型必須爲Lambda對應的接口類型,才能使用Lambda做爲該接口的實例。

備註:有且僅有一個抽象方法的接口,稱爲「函數式接口」。

相關文章
相關標籤/搜索