線程:多個任務同時進行,看似多任務同時進行,但實際上一個時間點上咱們大腦仍是隻在作一件事情。程序也是如此,除非多核cpu,否則一個cpu裏,在一個時間點裏仍是隻在作一件事,不過速度很快的切換,形成同時進行的錯覺。java
多線程:編程
方法間調用:普通方法調用,從哪裏來到哪裏去,是一條閉合的路徑;
使用多線程:開闢了多條路徑。設計模式
進程和線程:安全
也就是 Process 和 Thread ,本質來講,進程做爲資源分配的單位,線程是調度和執行的單位。具體來講:網絡
其餘概念:多線程
建立線程:併發
在 java 中,建立線程有 3 種方式:ide
根據設計原則,不論是里氏替換原則,仍是在工廠設計模式種,都提到過,儘可能多用實現,少用繼承,因此通常狀況下儘可能使用第二種方法建立線程。函數式編程
先直接看下面一個 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去安排時間片,調度決定的。
到這裏咱們總結使用第一種方法建立線程的步驟就是:
這種方法是推薦的方式,和上一種寫法相比較,很簡單,只須要把 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 "); } } }
總結第二種建立線程的方法步驟是:
特殊的,若是咱們的一個對象只使用一次,那就徹底能夠用匿名,上面的
StartThreadwithR startThread = new StartThreadwithR(); Thread t = new Thread(startThread); t.start();
能夠改爲:
new Thread(new StartThreadwithR()).start();
兩種方法相比,由於推薦優先實現接口,而不是繼承類,因此第二種方法是推薦的。
當多個線程同時進行修改資源的時候,可能出現線程不安全的問題,最上面咱們提到了,這裏作一個簡單模擬。
假如三個黃牛同時在搶票,服務端的票數--的過程,對於三個線程可能會出現哪些問題呢?
/* 使用多線程修改資源帶來的線程安全問題 */ 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(); } }
這裏面用了簡單的模擬服務端和客戶端行爲,請求票的時候,分別對票數進行 -- 操做,執行以後咱們來看:
顯然出現了邏輯上的錯誤,由於多個線程的執行帶來的問題。
從運行結果的最後兩行入手,背後的緣由是:
若是咱們再模擬一個網絡延遲,在 run 方法里加入:
//加入線程阻塞,模擬網絡延遲 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
多運行幾遍,甚至可能票數變成負數。
顯然,若是在實際開發中,票數的變化,應該是嚴格遞減的過程,而且,餘票到達 0 就應該 break,而不能還出現繼續執行了--操做,從而出現這種錯誤(不考慮退票之類的業務)。
這就是 高併發 問題,主要就是多線程帶來的安全問題。
再來看一個例子,假若有烏龜和兔子進行賽跑,咱們模擬兩個線程,分別對距離++。
/* 龜兔賽跑,藉助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.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 步:
能夠看到,這種方法的特殊之處在於:
注意到在前面使用第二種方法建立多線程的時候,提到了 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方法,可是不重要。
重要的是,代理類 可能作了不少的事,而中間須要 真實類 實現的一個方法必須實現,其餘的方法,真實類不須要關心,也就是交給代理類去辦了。
jdk1.8 後可使用 lambda 表達式來簡化代碼,通常用在 只使用一次的、簡單的線程 裏面。
簡化的寫法有不少,下面是逐漸簡化的過程。
若是某個類只但願使用一次,能夠用靜態內部類來實現,調用的時候同樣。
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(); } }
使用靜態內部類的好處是,不使用的時候這個內部類是不會編譯的,這其實就是一個單例模式。
還能夠直接寫到 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(); } }
更進一步,能夠直接利用匿名內部類,不用聲明出類的名稱來。
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方法是須要重寫/實現的。
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 方法。
顯然,若是不是線程,是其餘的咱們本身寫的接口+實現類,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方法,可使用簡化寫法。