1,單線程和多線程
我們通過一個實例來理解單線程和多線程。
假設有一個同學通訊錄,通訊錄長度為1000,用於記錄同學的姓名、電話、地址信息,用戶可以並發檢索該通訊錄,輸入通訊錄中的姓名,程序從通訊錄中查找該姓名,如果存在則輸出與該姓名相關的電話、地址信息。任務要求簡單模擬1000個用戶的並發訪問,檢索功能分別采用單線程和多線程實現,比較在1000個用戶的並發訪問下,單線程和多線程的檢索效率。
線程的創建和啟動
Java提供了兩種創建線程的方式。
一種方式是定義實現Java.lang.Runnable接口的類。Runnable接口中只有一個run()方法,用來定義線程運行體。代碼如下:

package com.example.demo; /** * Created with IntelliJ IDEA. * Author: zhangz * Date: 2019/1/8 * Time: 16:12 */ public class MyRunnable implements Runnable{ @Override public void run() { //在線程中執行的代碼 for(int i=0;i<100;i++){ System.out.println("MyRunner+"+i); } } }
定義好MyRunnable類后,需要把MyRunnable類的實例作為參數傳入到Thread的構造方法中,來創建一個新線程。然后在main方法中,實例化Thread對象,並將MyRunnable類的實例作為參數傳入進去,然后調用Thread對象的start方法啟動線程。代碼如下:

public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); /*for MyRunnable.java 實現Java.lang.Runnable接口的類*/ Thread thread = new Thread(new MyRunnable()); thread.start(); }
另外一種方式是將類定義為Thread類的子類,並重寫Thread類的run()方法,代碼如下:

public class MyThread extends Thread{ @Override public void run(){ //在線程中執行的代碼 for(int i=0;i<100;i++){ System.out.println("MyRunner+"+i); } } }
定義好Thread類的子類后,創建一個線程,只需要創建Thread子類的一個實例即可,然后在main方法中,只需實例化Thread對象即可,然后調用Thread對象的start方法啟動線程。代碼如下:

public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); /*for MyThread.java 將類定義為Thread類的子類,並重寫Thread類的run()方法*/ MyThread myThread =new MyThread(); myThread.start(); }
注意:在兩種創建線程的方式中,建議使用第一種方式。因為采用實現接口的方式可以避免由於Java的單一繼承帶來的局限,有利於代碼的健壯性。
下面,使用單線程來實現同學通訊錄檢索任務:
首先建立一個同學通訊錄類,代碼如下:

@Data public class PhoneBook { String name; String phone; String address; }
加上一個初始化通訊錄數據的昂發,用ArrayList集合類存儲1000個PhoneBook對象,代碼如下:

public void phoneBookInit(ArrayList<PhoneBook> inArray){ for(int i=0;i<1000;i++){ PhoneBook temp =new PhoneBook(); temp.setName("同學"+String.valueOf(i)); temp.setPhone("號碼"+String.valueOf(i)); temp.setAddress("地址"+String.valueOf(i)); inArray.add(temp); } }
用for循環模擬1000個並發客戶檢索通訊錄,並輸出通訊錄信息,記錄檢索全部完成時間,代碼如下:

public class PhoneSearch { public static void main(String[] args) { ArrayList<PhoneBook> array =new ArrayList<PhoneBook>(); PhoneSearch phoneSearch = new PhoneSearch(); phoneSearch.phoneBookInit(array); long start =System.currentTimeMillis(); for(int i=0;i<1000;i++){ phoneSearch.PhoneBookSearch("同學"+String.valueOf(i),array); } long end = System.currentTimeMillis(); long total = end-start; System.out.println("test spend time:------->"+total+"ms"); } public void PhoneBookSearch(String name,ArrayList<PhoneBook> inArray){ Iterator iterator = inArray.iterator(); int num =1; while(iterator.hasNext()){ PhoneBook temp =(PhoneBook) iterator.next(); if(temp.getName().equals(name)){ System.out.println("姓名:"+temp.getName()+"電話:"+temp.getPhone()+"地址:"+temp.getAddress()); return ; } } } public void phoneBookInit(ArrayList<PhoneBook> inArray){ for(int i=0;i<1000;i++){ PhoneBook temp =new PhoneBook(); temp.setName("同學"+String.valueOf(i)); temp.setPhone("號碼"+String.valueOf(i)); temp.setAddress("地址"+String.valueOf(i)); inArray.add(temp); } } }
程序用for循環模擬1000個並發客戶檢索通訊錄,在模擬檢索任務開始之前調用System的currentTimeMillis方法獲取系統當前時間,模擬檢索任務執行結束后,再獲取任務執行完成后的時間,然后計算兩個時間的差值,該差值就是檢索任務運行的時間。程序輸出結果如下圖所示:
耗時:178ms
接下來用多線程完成同學通訊錄檢索任務:
上面代碼PhoneSearch 類PhoenBookSearch方法完成通訊錄的檢索及信息輸出。下面的代碼把該方法改造為線程,這樣就可以實現當多用戶檢索通訊錄時,程序針對每個用戶的檢索請求,都會啟動一個線程去執行檢索任務,由順序執行改為並發執行。改造代碼如下:

package com.example.demo; import com.example.demo.entity.PhoneBook; import java.util.ArrayList; import java.util.Iterator; /** * Created with IntelliJ IDEA. * Author: zhangz * Date: 2019/1/8 * Time: 17:32 */ public class MutiThreadPhoneSearch implements Runnable{ ArrayList<PhoneBook> array; String name; public MutiThreadPhoneSearch(String inName,ArrayList<PhoneBook> inArray){ array = inArray; name = inName; } @Override public void run(){ Iterator iterator = array.iterator(); int num =1; while(iterator.hasNext()){ PhoneBook temp =(PhoneBook) iterator.next(); if(temp.getName().equals(name)){ System.out.println("姓名:"+temp.getName()+"電話:"+temp.getPhone()+"地址:"+temp.getAddress()); return ; } } } }
代碼定義一個MutiThreadPhoneSearch,該類實現Runnable接口,並重寫Runnable接口的run()方法,在run方法中,完成通訊錄的檢索及輸出功能。
在main方法中,不再調用PhoenBookSearch方法,而是實例化Thread對象,並將MutiThreadPhoneSearch類的實例作為參數傳入進去,然后調用Thread對象的start方法啟動線程,代碼如下:

public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); /*for MutiThreadPhoneSearch.java 多線程實現*/ ArrayList<PhoneBook> array =new ArrayList<PhoneBook>(); PhoneSearch phoneSearch = new PhoneSearch(); phoneSearch.phoneBookInit(array); long start =System.currentTimeMillis(); for(int i=0;i<1000;i++){ MutiThreadPhoneSearch mutiThreadPhoneSearch = new MutiThreadPhoneSearch("同學"+String.valueOf(i),array); Thread thread = new Thread(mutiThreadPhoneSearch); thread.start(); } long end = System.currentTimeMillis(); long total = end-start; System.out.println("test spend time:------->"+total+"ms"); } public void phoneBookInit(ArrayList<PhoneBook> inArray){ for(int i=0;i<1000;i++){ PhoneBook temp =new PhoneBook(); temp.setName("同學"+String.valueOf(i)); temp.setPhone("號碼"+String.valueOf(i)); temp.setAddress("地址"+String.valueOf(i)); inArray.add(temp); } } }
執行結果如下:
耗時278ms,還沒執行完。
從輸出結果看,檢索結果並沒有按照順序輸出,整個檢索耗時278ms。用多線程技術實現通訊錄的並發檢索,並沒有提高檢索效率,反而不如單線程的運行速度快。主要原因是系統每啟動一個線程,都要耗費一定的系統資源,導致運行效率降低,多線程在這個例子程序中,並沒有體現出多線程的性能優勢。
我們換個場景,假如把通訊錄的檢索放到服務器端,1000個用戶在同一時間並發檢索通訊錄,如果服務端是單線程服務,雖然1000個用戶是並發訪問,但要在服務器端隨機排隊等候服務器響應,如果1個用戶的響應時間為1秒,那么依次類推,最后1個用戶的響應時間為1000秒。如果是多線程服務,平均每個用戶的響應時間為2到3秒左右,顯然能夠滿足大多數用戶的響應需求。在這個場景下,多線程就體現出了性能優勢。
在正常情況下,讓程序來完成多個任務,只使用單個線程來完成比用多個線程完成所用的時間會更短。因為JVM在調度管理每個線程上肯定要花費一定資源和時間的。那么,在什么場景下使用多線程呢?一是對用戶響應要求比較高,又允許用戶並發訪問的場景;二是程序存在耗費時間的計算,整個系統都會等待這個操作,為了提高程序的響應,將耗費時間的計算通過線程來完成。
線程池及其特點,場景
線程池作用就是限制系統中執行線程的數量。根據系統的環境情況,可以自動或手動設置線程數量,達到運行的最佳效果;少了浪費了系統資源,多了造成系統擁擠效率不高。用線程池控制線程數量,其他線程排隊等候。一個任務執行完畢,再從隊列的中取最前面的任務開始執行。若隊列中沒有等待進程,線程池的這一資源處於等待。當一個新任務需要運行時,如果線程池中有等待的工作線程,就可以開始運行了;否則進入等待隊列。
為什么要用線程池:
1.減少了創建和銷毀線程的次數,每個工作線程都可以被重復利用,可執行多個任務。
2.可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因為消耗過多的內存,而把服務器累趴下(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,最后死機)。
如果用生活中的列子來說明,我們可以把線程池當做一個客服團隊,如果同時有1000個人打電話進行咨詢,按照正常的邏輯那就是需要1000個客服接聽電話,服務客戶。現實往往需要考慮到很多層面的東西,比如:資源夠不夠,招這么多人需要費用比較多。正常的做法就是招100個人成立一個客服中心,當有電話進來后分配沒有接聽的客服進行服務,如果超出了100個人同時咨詢的話,提示客戶等待,稍后處理,等有客服空出來就可以繼續服務下一個客戶,這樣才能達到一個資源的合理利用,實現效益的最大化。
Java中的線程池種類
1. newSingleThreadExecutor
創建方式:
ExecutorService pool = Executors.newSingleThreadExecutor();
一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因為異常結束,那么會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。
使用方式:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPool {
public static void main(String[] args) {
ExecutorService pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t開始發車啦....");
});
}
}
}
輸出結果如下:
pool-1-thread-1 開始發車啦....
pool-1-thread-1 開始發車啦....
pool-1-thread-1 開始發車啦....
pool-1-thread-1 開始發車啦....
pool-1-thread-1 開始發車啦....
pool-1-thread-1 開始發車啦....
pool-1-thread-1 開始發車啦....
pool-1-thread-1 開始發車啦....
pool-1-thread-1 開始發車啦....
pool-1-thread-1 開始發車啦....
從輸出的結果我們可以看出,一直只有一個線程在運行。
2.newFixedThreadPool
創建方式:
ExecutorService pool = Executors.newFixedThreadPool(10);
創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因為執行異常而結束,那么線程池會補充一個新線程。
使用方式:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPool {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t開始發車啦....");
});
}
}
}
輸出結果如下:
pool-1-thread-1 開始發車啦....
pool-1-thread-4 開始發車啦....
pool-1-thread-3 開始發車啦....
pool-1-thread-2 開始發車啦....
pool-1-thread-6 開始發車啦....
pool-1-thread-7 開始發車啦....
pool-1-thread-5 開始發車啦....
pool-1-thread-8 開始發車啦....
pool-1-thread-9 開始發車啦....
pool-1-thread-10 開始發車啦....
3. newCachedThreadPool
創建方式:
ExecutorService pool = Executors.newCachedThreadPool();
創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那么就會回收部分空閑的線程,當任務數增加時,此線程池又添加新線程來處理任務。
使用方式如上2所示。
4.newScheduledThreadPool
創建方式:
ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
此線程池支持定時以及周期性執行任務的需求。
使用方式:
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ThreadPool {
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
for (int i = 0; i < 10; i++) {
pool.schedule(() -> {
System.out.println(Thread.currentThread().getName() + "\t開始發車啦....");
}, 10, TimeUnit.SECONDS);
}
}
}
上面演示的是延遲10秒執行任務,如果想要執行周期性的任務可以用下面的方式,每秒執行一次
//pool.scheduleWithFixedDelay也可以
pool.scheduleAtFixedRate(() -> {
System.out.println(Thread.currentThread().getName() + "\t開始發車啦....");
}, 1, 1, TimeUnit.SECONDS);
5.newWorkStealingPool
newWorkStealingPool是jdk1.8才有的,會根據所需的並行層次來動態創建和關閉線程,通過使用多個隊列減少競爭,底層用的ForkJoinPool來實現的。ForkJoinPool的優勢在於,可以充分利用多cpu,多核cpu的優勢,把一個任務拆分成多個“小任務”,把多個“小任務”放到多個處理器核心上並行執行;當多個“小任務”執行完成之后,再將這些執行結果合並起來即可。