java的線程、建立線程的 3 種方式、靜態代理模式、Lambda表達式簡化線程


0、介紹


線程多個任務同時進行,看似多任務同時進行,但實際上一個時間點上咱們大腦仍是隻在作一件事情。程序也是如此,除非多核cpu,否則一個cpu裏,在一個時間點裏仍是隻在作一件事,不過速度很快的切換,形成同時進行的錯覺。java

多線程編程

方法間調用:普通方法調用,從哪裏來到哪裏去,是一條閉合的路徑;
使用多線程:開闢了多條路徑。設計模式

進程和線程安全

也就是 Process 和 Thread ,本質來講,進程做爲資源分配的單位,線程是調度和執行的單位。具體來講:網絡

  • 每一個進程都有獨立的代碼和數據空間(進程上下文),進程間切換會有較大開銷,操做系統中同時運行多個任務就是進程;
  • 線程能夠當作輕量級的線程,同一類線程共享代碼和數據空間,每一個線程有獨立的運行棧和程序計數器(PC),線程切換的開銷較小,同一個應用程序裏多個順序流在執行,他們就是線程,除了CPU外,不會爲線程分配內存,它本身使用的是所屬進程的資源,線程組只能共享資源。

其餘概念多線程

  • 線程能夠理解爲一個獨立的執行路徑;
  • 在程序運行的時候,即便沒有本身建立線程,後臺也會存在gc線程、主線程等,而main() 就是主線程,是程序的入口點;
  • 一個進程裏若是開闢了多個線程,線程一旦開始運行,是由調度器安排的,和操做系統緊密相關,他們的安排人爲無法干預;
  • 對於同一份資源操做,會涉及資源搶奪問題,須要加入併發控制;
  • 線程會帶來cpu調度時間、併發控制等額外的開銷;
  • 每一個線程只在本身的工做內存交互,若是加載和存儲主內存控制不當,就會形成數據不一致,也就是線程不安全。

建立線程併發

在 java 中,建立線程有 3 種方式:ide

  1. 繼承Thread類(重寫run方法)
  2. 實現Runnable接口(重寫run方法)
  3. 實現Callable接口(重寫call方法,這個是在j.u.c包下的)

根據設計原則,不論是里氏替換原則,仍是在工廠設計模式種,都提到過,儘可能多用實現,少用繼承,因此通常狀況下儘可能使用第二種方法建立線程。函數式編程


1、建立方法1:繼承Thread類


先直接看下面一個 demo函數

/*
    建立方式1:繼承Thread + 重寫run
    啓動方式:建立子類對象 + start
*/
public class StartThread extends Thread {
    //線程入口點
    @Override
    public void run() {
        for (int i=0; i<50; i++){
            System.out.print("睡覺ing ");
        }
    }

    public static void main(String[] args) {
        //建立子類對象
        StartThread startThread = new StartThread();
        //啓動,主意是start
        startThread.start();
        for (int i=0; i<50; i++){
            System.out.print("吃飯ing ");
        }
    }
}

咱們把上面的run方法成爲線程的入口點,裏面是線程執行的代碼,當程序運行以後,能夠發現,每次的運行結果都是不同的。

能夠看到這種隨機穿插執行的結果,這是由cpu去安排時間片,調度決定的

到這裏咱們總結使用第一種方法建立線程的步驟就是:

  1. 建立子類對象,這個子類是繼承了Thread類的;
  2. 啓動,調用start方法,而不是run方法,start方法是把這個線程丟給cpu的調度器,讓他適時運行而不是當即運行。若是使用run方法,那麼就是單純的執行,並無開啓多線程,會先執行完上面的內容,再往下走。

2、建立方法2:實現Runnable接口


這種方法是推薦的方式,和上一種寫法相比較,很簡單,只須要把 extends Thread 改爲 implements Runnable ,其餘的地方几乎沒有變化。

區別在於,調用的時候,不能直接 start(),只能藉助一個 Thread 對象做爲代理。

/*
    建立方式2:實現Runnable + 重寫run
    啓動方式:建立實現類對象 + 藉助thread代理類 + start
*/
public class StartThreadwithR implements Runnable {
    @Override
    public void run() {
        for (int i=0; i<50; i++){
            System.out.print("睡覺ing ");
        }
    }

    public static void main(String[] args) {
        StartThreadwithR startThread = new StartThreadwithR();
        //建立代理類
        Thread t = new Thread(startThread);
        t.start();//啓動
        for (int i=0; i<50; i++){
            System.out.print("吃飯ing ");
        }
    }
}

總結第二種建立線程的方法步驟是:

  1. 建立實現類對象,實現類實現的是Runnable接口;
  2. 建立代理類Thread
  3. 將實現類對象丟給代理類,而後用代理類start。

特殊的,若是咱們的一個對象只使用一次,那就徹底能夠用匿名,上面的

StartThreadwithR startThread = new StartThreadwithR();
        Thread t = new Thread(startThread);
        t.start();

能夠改爲:

new Thread(new StartThreadwithR()).start();

兩種方法相比,由於推薦優先實現接口,而不是繼承類,因此第二種方法是推薦的。


3、可能出現的問題


3.1 黃牛訂票

當多個線程同時進行修改資源的時候,可能出現線程不安全的問題,最上面咱們提到了,這裏作一個簡單模擬。

假如三個黃牛同時在搶票,服務端的票數--的過程,對於三個線程可能會出現哪些問題呢?

/*
    使用多線程修改資源帶來的線程安全問題
*/
public class Tickets implements Runnable{
    private int ticketNum = 100;
    @Override
    public void run() {
        while(true){
            if (ticketNum<0){
                break;
            }
            System.out.println(Thread.currentThread().getName() + "正在搶票,餘票" + ticketNum--);
        }
    }
    //客戶端
    public static void main(String[] args) {
        Tickets tickets = new Tickets();
        //多個Thread代理
        new Thread(tickets,"黃牛1").start();
        new Thread(tickets,"黃牛2").start();
        new Thread(tickets,"黃牛3").start();
    }
}

這裏面用了簡單的模擬服務端和客戶端行爲,請求票的時候,分別對票數進行 -- 操做,執行以後咱們來看:

顯然出現了邏輯上的錯誤,由於多個線程的執行帶來的問題。

從運行結果的最後兩行入手,背後的緣由是:

  1. 黃牛 2 先進入run;
  2. 但是到將票數-1以前,因爲cpu的調度,黃牛 3 線程也開始執行,而且比黃牛 2 更快一步,直接進行了 -- 操做,票數變成了 0 ;
  3. 此時黃牛 2 輸出告終果,餘票0;
  4. 隨後黃牛 3 線程才執行完輸出語句,票數反卻是 1 ?

若是咱們再模擬一個網絡延遲,在 run 方法里加入:

//加入線程阻塞,模擬網絡延遲
try {
    Thread.sleep(200);
} catch (InterruptedException e) {
    e.printStackTrace();
}

多運行幾遍,甚至可能票數變成負數。

顯然,若是在實際開發中,票數的變化,應該是嚴格遞減的過程,而且,餘票到達 0 就應該 break,而不能還出現繼續執行了--操做,從而出現這種錯誤(不考慮退票之類的業務)。

這就是 高併發 問題,主要就是多線程帶來的安全問題。


3.2 龜兔賽跑

再來看一個例子,假若有烏龜和兔子進行賽跑,咱們模擬兩個線程,分別對距離++。

/*
    龜兔賽跑,藉助Runnable和Thread代理
*/
public class Racer implements Runnable{
    private String winner;
    @Override
    public void run() {
        for (int dis=1; dis<=100; dis++){
            System.out.println(Thread.currentThread().getName() + " 跑了 " + dis);
            //每走一步,判斷是否比賽結束
            if (gameOver(dis))break;
        }
    }

    public boolean gameOver(int dis){
        if (winner != null){
            return true;
        } else if (dis == 100){
            winner = Thread.currentThread().getName();
            System.out.println("獲勝者是 "+winner);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        Racer racer = new Racer();//1.建立實現類
        new Thread(racer,"兔子").start();//2.建立代理類並start
        new Thread(racer,"烏龜").start();
    }
}

這樣運行起來,總會有一我的贏,可是贏的每次不必定是哪個。


4、建立方法3:實現Callable


面對高併發的狀況,須要用到線程池。

來看從新實現的龜兔賽跑:

/*
    建立方法3:Callable,是java.util.concurrent包裏的內容
*/
public class RacerwithCal implements Callable<Integer> {
    private String winner;

    //須要實現的是call方法
    @Override
    public Integer call() throws Exception {
        for (int dis=1; dis<=100; dis++){
            System.out.println(Thread.currentThread().getName() + " 跑了 " + dis);
            //每走一步,判斷是否比賽結束,而且結束能夠有返回值
            if (gameOver(dis))return dis;
        }
        return null;
    }

    public boolean gameOver(int dis){
        if (winner != null){
            return true;
        } else if (dis == 100){
            winner = Thread.currentThread().getName();
            if (winner.equals("pool-1-thread-1"))System.out.println("獲勝者是 烏龜");
            else System.out.println("獲勝者是 兔子");
            return true;
        }
        return false;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1.建立目標對象
        RacerwithCal race = new RacerwithCal();
        //2.建立執行服務,含有2個線程的線程池
        ExecutorService service = Executors.newFixedThreadPool(2);
        //3.提交執行
        Future<Integer> result1 = service.submit(race);
        Future<Integer> result2 = service.submit(race);
        //4.獲取結果:pool-1-thread-1也就是第一個線程是烏龜,第二個兔子
        Integer i = result1.get();
        Integer j = result2.get();
        System.out.println("比分是: "+ i + " : " + j);
        //5.關閉服務
        service.shutdownNow();
    }
}

來看執行結果:

總結一下,步驟通常分爲 5 步:

  1. 建立目標對象;
  2. 建立執行服務;
  3. 提交執行;
  4. 獲取結果;
  5. 關閉服務。

能夠看到,這種方法的特殊之處在於:

  • 目標類繼實現Callable接口的 call 方法,能夠有返回值(前面的run是沒有返回值的);
  • 不用處理異常,能夠直接 throw;
  • 使用的過程相比前兩種方法,變得複雜。

5、靜態代理模式


注意到在前面使用第二種方法建立多線程的時候,提到了 new Thread(tickets,"黃牛1").start(); 是使用了 Thread 做爲代理。代理模式自己也是設計模式種的一種,分爲動態代理和靜態代理,代理模式在開發中記錄日誌等等很經常使用。

靜態代理的代理類是直接寫好的,拿過來用,動態代理則是在程序執行過程當中臨時建立的。

在這裏簡單介紹靜態代理。

實現一個婚慶公司,做爲你的婚禮的代理,而後進行婚禮舉辦。

/*
    靜態代理模式demo
    1.真實角色
    2.代理角色
    3.1和2都實現同一個接口
*/
public class StaticProxy {
    public static void main(String[] args) {
        //徹底相似於 new Thread(new XXX()).start();
        new WeddingCompany(new You()).wedding();
    }
}

//接口
interface Marry{
    void wedding();
}

//真實角色
class You implements Marry{
    @Override
    public void wedding() {
        System.out.println("結婚路上ing");
    }
}

//代理角色
class WeddingCompany implements Marry{
    //要代理的真實角色
    private Marry target;
    public WeddingCompany(Marry target) {
        this.target = target;
    }

    @Override
    public void wedding() {
        ready();//準備
        this.target.wedding();
        after();//善後
    }
    private void after() {
        System.out.println("結束ing");
    }
    private void ready() {
        System.out.println("佈置ing");
    }
}

能夠看到,最後的調用方法就至關因而寫線程的時候用到的 new Thread(new XXX()).start();

小小區別就在於,咱們寫的線程類是實現的 run 方法,沒有實現start方法,可是不重要。

重要的是,代理類 可能作了不少的事,而中間須要 真實類 實現的一個方法必須實現,其餘的方法,真實類不須要關心,也就是交給代理類去辦了。


6、Lambda表達式簡化線程


jdk1.8 後可使用 lambda 表達式來簡化代碼,通常用在 只使用一次的、簡單的線程 裏面。

簡化的寫法有不少,下面是逐漸簡化的過程。


6.1 靜態內部類

若是某個類只但願使用一次,能夠用靜態內部類來實現,調用的時候同樣。

public class StartThreadLambda {
    //靜態內部類
    static class Inner implements Runnable{
        @Override
        public void run() {
            for (int i=0; i<50; i++){
                System.out.print("睡覺ing ");
            }
        }
    }
    //靜態內部類
    static class Inner2 implements Runnable{
        @Override
        public void run() {
            for (int i=0; i<50; i++){
                System.out.print("吃飯ing ");
            }
        }
    }
    public static void main(String[] args) {
        new Thread(new Inner()).start();
        new Thread(new Inner2()).start();
    }
}

使用靜態內部類的好處是,不使用的時候這個內部類是不會編譯的,這其實就是一個單例模式。


6.2 方法內部類

還能夠直接寫到 main 方法內部,由於main 方法就是static,只啓動一次。

public class StartThreadLambda {
    public static void main(String[] args) {
        //方法內部類(局部內部類)
        class Inner implements Runnable{
                    //。。。。。。
        }
        class Inner2 implements Runnable{
                    //。。。。。。
        }
        new Thread(new Inner()).start();
        new Thread(new Inner2()).start();
    }
}

6.3 匿名內部類

更進一步,能夠直接利用匿名內部類,不用聲明出類的名稱來。

public class StartThreadLambda {
    public static void main(String[] args) {
        //匿名內部類,必須藉助接口或者父類,由於沒有名字
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i=0; i<50; i++){
                    System.out.print("吃飯ing ");
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i=0; i<50; i++){
                    System.out.print("睡覺ing ");
                }
            }
        }).start();
    }
}

這裏面必須帶上實現體了就,由於沒有名字,那麼就要藉助父類或者接口,而父類或者接口的run方法是須要重寫/實現的。


6.4 Lambda表達式

jdk 8 對匿名內部類寫法再進行簡化,只用關注線程體,也就是隻關注 run 方法裏面的內容。

public class StartThreadLambda {
    public static void main(String[] args) {
        //使用Lambda表達式
        new Thread(()-> {
            for (int i=0; i<50; i++){
                System.out.print("吃飯ing ");
            }
        }).start();

        new Thread(()->{
            for (int i=0; i<50; i++){
                System.out.print("睡覺ing ");
            }
        }).start();
    }
}

() - > 這個符號,編譯器就默認你是在實現 Runnable,而且默認是在實現 run 方法。


6.5 擴展

顯然,若是不是線程,是其餘的咱們本身寫的接口+實現類,Lambda表達式也是可用的,並且能夠進行參數和返回值的擴展。

public class LambdaTest {
    public static void main(String[] args) {
        //直接使用lambda表達式實現接口
        Origin o = (int a, int b)-> {
            return a+b;
        };
        System.out.println(o.sum(100,100));
    }
}

//自定義接口,至關於Runnable
interface Origin{
    int sum(int a, int b);
}

更有甚者,參數的類型也能夠省略,他會本身去匹配:

//省略參數類型
Origin o1 = (a, b) -> {
    return a+b;
};

若是實現接口的方法,只有一行代碼,甚至花括號也能夠省略:

Origin o2 = (a, b) -> a+b;

有關返回值和參數的個數仍是有一些細微差異的。

Lambda表達式也在 Sort 方法裏有應用,要想對引用類型裏面統一按照某個屬性進行排序,須要實現Comparator接口裏面的compare方法,可使用簡化寫法。

  • Lambda 表達式的支持,主要是爲了避免匿名內部類定義過多,實質上是屬於函數式編程的概念
  • 須要注意的是,Lambda表達式只支持實現一個方法。
相關文章
相關標籤/搜索