1. 程式人生 > >java的執行緒、建立執行緒的 3 種方式、靜態代理模式、Lambda表示式簡化執行緒

java的執行緒、建立執行緒的 3 種方式、靜態代理模式、Lambda表示式簡化執行緒


# 0、介紹
**執行緒**:多個任務同時進行,看似多工同時進行,但實際上一個時間點上我們大腦還是隻在做一件事情。程式也是如此,除非多核cpu,不然一個cpu裡,在一個時間點裡還是隻在做一件事,不過速度很快的切換,造成同時進行的錯覺。 **多執行緒**: 方法間呼叫:普通方法呼叫,從哪裡來到哪裡去,是一條閉合的路徑; 使用多執行緒:開闢了多條路徑。 **程序和執行緒**: 也就是 Process 和 Thread ,本質來說,程序作為資源分配的單位,執行緒是排程和執行的單位。具體來說: * 每個程序都有獨立的程式碼和資料空間(程序上下文),程序間切換會有較大開銷,作業系統中同時執行多個任務就是程序; * 執行緒可以看成輕量級的執行緒,同一類執行緒共享程式碼和資料空間,每個執行緒有獨立的執行棧和程式計數器(PC),執行緒切換的開銷較小,同一個應用程式裡多個順序流在執行,他們就是執行緒,除了CPU外,不會為執行緒分配記憶體,它自己使用的是所屬程序的資源,執行緒組只能共享資源。 **其他概念**: * 執行緒可以理解為一個獨立的執行路徑; * 在程式執行的時候,即使沒有自己建立執行緒,後臺也會存在gc執行緒、主執行緒等,而main() 就是主執行緒,是程式的入口點; * 一個程序裡如果開闢了多個執行緒,執行緒一旦開始執行,是由排程器安排的,和作業系統緊密相關,他們的安排人為沒法干預; * 對於同一份資源操作,會涉及資源搶奪問題,需要加入併發控制; * 執行緒會帶來cpu排程時間、併發控制等額外的開銷; * 每個執行緒只在自己的工作記憶體互動,如果載入和儲存主記憶體控制不當,就會造成資料不一致,也就是執行緒不安全。 **建立執行緒**: **在 java 中,建立執行緒有 3 種方式:** 1. **繼承Thread類(重寫run方法)**; 2. **實現Runnable介面(重寫run方法)**; 3. **實現Callable介面(重寫call方法,這個是在j.u.c包下的)**。 根據設計原則,不管是里氏替換原則,還是在工廠設計模式種,都提到過,儘量多用實現,少用繼承,所以**一般情況下儘量使用第二種方法建立執行緒。**
# 一、建立方法1:繼承Thread類
先直接看下面一個 demo ```java /* 建立方式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:實現Runnable介面
這種方法是推薦的方式,和上一種寫法相比較,很簡單,只需要把 extends Thread 改成 implements Runnable ,其他的地方几乎沒有變化。 區別在於,呼叫的時候,不能直接 start(),只能藉助一個 Thread 物件作為代理。 ```java /* 建立方式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。 特殊的,如果我們的一個物件只使用一次,那就完全可以用匿名,上面的 ```java StartThreadwithR startThread = new StartThreadwithR(); Thread t = new Thread(startThread); t.start(); ``` 可以改成: ```java new Thread(new StartThreadwithR()).start(); ``` 兩種方法相比,因為推薦優先實現介面,而不是繼承類,所以第二種方法是推薦的。
# 三、可能出現的問題
## 3.1 黃牛訂票 當多個執行緒同時進行修改資源的時候,可能出現執行緒不安全的問題,最上面我們提到了,這裡做一個簡單模擬。 假如三個黃牛同時在搶票,服務端的票數--的過程,對於三個執行緒可能會出現哪些問題呢? ```java /* 使用多執行緒修改資源帶來的執行緒安全問題 */ 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 方法里加入: ```java //加入執行緒阻塞,模擬網路延遲 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } ``` 多執行幾遍,甚至可能票數變成負數。
顯然,如果在實際開發中,票數的變化,應該是嚴格遞減的過程,並且,餘票到達 0 就應該 break,而不能還出現繼續執行了--操作,從而出現這種錯誤(不考慮退票之類的業務)。 這就是 **高併發** 問題,主要就是**多執行緒帶來的安全**問題。
## 3.2 龜兔賽跑 再來看一個例子,假如有烏龜和兔子進行賽跑,我們模擬兩個執行緒,分別對距離++。 ```java /* 龜兔賽跑,藉助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(); } } ``` 這樣執行起來,總會有一個人贏,但是贏的每次不一定是哪一個。
# 四、建立方法3:實現Callable
面對高併發的情況,需要用到執行緒池。 來看重新實現的龜兔賽跑: ```java /* 建立方法3:Callable,是java.util.concurrent包裡的內容 */ public class RacerwithCal implements Callable { 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 result1 = service.submit(race); Future 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; - 使用的過程相比前兩種方法,變得複雜。
# 五、靜態代理模式
注意到在前面使用第二種方法建立多執行緒的時候,提到了 new Thread(tickets,"黃牛1").start(); **是使用了 Thread 作為代理**。代理模式本身也是設計模式種的一種,分為動態代理和靜態代理,代理模式在開發中記錄日誌等等很常用。 靜態代理的代理類是直接寫好的,拿過來用,動態代理則是在程式執行過程中臨時建立的。 在這裡簡單介紹**靜態代理。** 實現一個婚慶公司,作為你的婚禮的代理,然後進行婚禮舉辦。 ```java /* 靜態代理模式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方法,但是不重要。 重要的是,**代理類** 可能做了很多的事,而中間需要 **真實類** 實現的一個方法必須實現,其他的方法,真實類不需要關心,也就是交給代理類去辦了。
# 六、Lambda表示式簡化執行緒
jdk1.8 後可以使用 lambda 表示式來簡化程式碼,一般用在 **只使用一次的、簡單的執行緒** 裡面。 簡化的寫法有很多,下面是逐漸簡化的過程。
## 6.1 靜態內部類 如果某個類只希望使用一次,可以用靜態內部類來實現,呼叫的時候一樣。 ```java 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,只啟動一次。 ```java 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 匿名內部類 更進一步,可以直接利用匿名內部類,不用宣告出類的名稱來。 ```java 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 方法裡面的內容。 ```java 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表示式也是可用的,而且可以進行引數和返回值的擴充套件。 ```java 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); } ``` 更有甚者,引數的型別也可以省略,他會自己去匹配: ```java //省略引數型別 Origin o1 = (a, b) -> { return a+b; }; ``` 如果實現介面的方法,只有一行程式碼,甚至花括號也可以省略: ```java Origin o2 = (a, b) -> a+b; ``` 有關返回值和引數的個數還是有一些細微差別的。 Lambda表示式也在 Sort 方法裡有應用,要想對引用型別裡面統一按照某個屬性進行排序,需要實現Comparator接口裡面的compare方法,可以使用簡化寫法。 * Lambda 表示式的支援,主要是為了**避免匿名內部類定義過多,實質上是屬於函數語言程式設計的概念**。 * 需要注意的是,Lambda表示式只支援實現一個