目錄
二、多線程的實現(xiàn)方式(四種)
三、線程池的創(chuàng)建使用(五種)
1、newFixedThreadPool定長線程池
2、CachedThreadPool可緩存線程池
3、newSingleThreadExecutor單線程化線程池
4、newScheduledThreadPool周期性線程池
無返回值的周期性線程池
有返回值的周期性線程池
5、ThreadPoolExecutor(手動創(chuàng)建線程池)
線程池的submit和execute方法區(qū)別
一、初認(rèn)多線程
1、什么是線程
進(jìn)程是:一個應(yīng)用程序(1個進(jìn)程是一個軟件)。
線程是:一個進(jìn)程中的執(zhí)行場景/執(zhí)行單元。
注意: 一個進(jìn)程可以啟動多個線程。
java主線程: 每個java程序都含有一個線程,那就是主線程(main線程)。Java應(yīng)用程序都是從主類main方法執(zhí)行的,當(dāng)jvm加載代碼,發(fā)現(xiàn)賣弄方法之后,就會啟動一個線程,這個線程就是主線程,負(fù)責(zé)執(zhí)行main方法。如果在主線程里面創(chuàng)建其他線程,就會在主線程和其他線程來回切換,直到其他所有線程結(jié)束才會結(jié)束主線程。
所謂多線程,就是說一個應(yīng)用程序有多條執(zhí)行路徑,每當(dāng)我們打開一個應(yīng)用程序的時候,就相打開了一個進(jìn)程,而進(jìn)程中執(zhí)行的操作(這就是一條線程對應(yīng)用程序進(jìn)行訪問),就是線程。以迅雷為例,打開迅雷就相當(dāng)于打開一個進(jìn)程,下載文件的操作就是線程,多線程就是同時下載多個文件。
以接口為例,當(dāng)許多人同時調(diào)用一個接口,我們就可以把每一個人看做一條線程去調(diào)用我們的接口。
二、多線程的實現(xiàn)方式(四種)
1、繼承 Thread 類
通過繼承 Thread 類似實現(xiàn)多線程的步驟如下:
public class MyThread extends Thread { @Override public void run() { System.out.println(“我是通過繼承 Thread 類創(chuàng)建的多線程,我叫” + Thread.currentThread().getName()); }}class TestMyThread { public static void main(String[] args) { MyThread myThread1 = new MyThread(); myThread1.setName(“Thread-1”); MyThread myThread2 = new MyThread(); myThread2.setName(“Thread-2”); MyThread myThread3 = new MyThread(); myThread3.setName(“Thread-3”); myThread1.start(); myThread2.start(); myThread3.start(); }}
為了演示線程執(zhí)行順序的隨機性,我特意創(chuàng)建了三個線程,并為每一個線程命名,下面是我運行五次程序的執(zhí)行結(jié)果:
// 第一次我是通過繼承 Thread 類創(chuàng)建的多線程,我叫Thread-2我是通過繼承 Thread 類創(chuàng)建的多線程,我叫Thread-1我是通過繼承 Thread 類創(chuàng)建的多線程,我叫Thread-3// 第二次我是通過繼承 Thread 類創(chuàng)建的多線程,我叫Thread-1我是通過繼承 Thread 類創(chuàng)建的多線程,我叫Thread-3我是通過繼承 Thread 類創(chuàng)建的多線程,我叫Thread-2// 第三次我是通過繼承 Thread 類創(chuàng)建的多線程,我叫Thread-1我是通過繼承 Thread 類創(chuàng)建的多線程,我叫Thread-3我是通過繼承 Thread 類創(chuàng)建的多線程,我叫Thread-2
從上面的執(zhí)行結(jié)果我們可以看到線程的執(zhí)行順序和代碼中編寫的順序沒有關(guān)系,線程的執(zhí)行順序是具有隨機性的。
2、實現(xiàn) Runnable 接口
通過實現(xiàn) Runnable 接口實現(xiàn)多線程的步驟如下:
1.創(chuàng)建 MyRunnable 類實現(xiàn) Runnable 接口。
2.創(chuàng)建 MyRunnable 類的實例對象 myRunnable 。
3.把實例對象 myRunnable 作為參數(shù)來創(chuàng)建 Thread 類的實例對象 thread,實例對象 thread 就是一個新線程。
4.調(diào)用 start() 方法,啟動線程。、
public class RunnableTest implements Runnable{ @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("我是通過實現(xiàn) Runnable 接口創(chuàng)建的多線程,我叫" + Thread.currentThread().getName()); } } } class Test { public static void main(String[] args) { Thread myThread1 = new Thread(new RunnableTest()); Thread myThread2 = new Thread(new RunnableTest()); myThread1.start(); myThread2.start(); }}
執(zhí)行結(jié)果如下:
我是通過實現(xiàn) Runnable 接口創(chuàng)建的多線程,我叫Thread-1我是通過實現(xiàn) Runnable 接口創(chuàng)建的多線程,我叫Thread-0我是通過實現(xiàn) Runnable 接口創(chuàng)建的多線程,我叫Thread-1我是通過實現(xiàn) Runnable 接口創(chuàng)建的多線程,我叫Thread-0我是通過實現(xiàn) Runnable 接口創(chuàng)建的多線程,我叫Thread-1我是通過實現(xiàn) Runnable 接口創(chuàng)建的多線程,我叫Thread-0我是通過實現(xiàn) Runnable 接口創(chuàng)建的多線程,我叫Thread-1我是通過實現(xiàn) Runnable 接口創(chuàng)建的多線程,我叫Thread-0我是通過實現(xiàn) Runnable 接口創(chuàng)建的多線程,我叫Thread-1我是通過實現(xiàn) Runnable 接口創(chuàng)建的多線程,我叫Thread-0
注:
相比于繼承 Thread 類的方法來說,實現(xiàn) Runnable 接口是一個更好地選擇,因為 Java 不支持多繼承,但是可以實現(xiàn)多個接口。
有一點值得注意的是 Thread 類也實現(xiàn)了 Runnable 接口,這意味著構(gòu)造函數(shù) Thread(Runnable target) 不僅可以傳入 Runnable 接口的對象,而且可以傳入一個 Thread 類的對象,這樣就可以將一個 Thread 對象中的 run() 方法交由其他線程進(jìn)行調(diào)用。
3、實現(xiàn) Callable 接口
Callable 接口只有一個 call() 方法,源碼如下:
public interface Callable { V call() throws Exception;}
從源碼我們可以看到 Callable 接口和 Runnable 接口類似,它們之間的區(qū)別在于 run() 方法沒有返回值,而 call() 方法是有返回值的。
通過實現(xiàn) Callable 接口實現(xiàn)多線程的步驟如下:
1.創(chuàng)建 MyCallable 類實現(xiàn) Callable 接口。
2.創(chuàng)建 MyCallable 類的實例對象 myCallable。
3.把實例對象 myCallable 作為參數(shù)來創(chuàng)建 FutureTask 類的實例對象 futureTask。
4.把實例對象 futureTask 作為參數(shù)來創(chuàng)建 Thread 類的實例對象 thread,實例對象 thread 就是一個新線程。
5.調(diào)用 start() 方法,啟動線程。
public class CallbaleTest implements Callable { @Override public Integer call() throws Exception { int a = 6; int b = 9; System.out.println(“我是通過實現(xiàn) Callable 接口創(chuàng)建的多線程,我叫” + Thread.currentThread().getName()); return a + b; } } class TestMyCallable { public static void main(String[] args) throws ExecutionException, InterruptedException { CallbaleTest myCallable = new CallbaleTest(); FutureTask futureTask = new FutureTask(myCallable); Thread thread = new Thread(futureTask); Thread thread1 = new Thread(futureTask); thread.start(); thread1.start(); Integer integer = futureTask.get(); System.out.println(“返回值為:” + integer); } }
執(zhí)行后的結(jié)果如下:
我是通過實現(xiàn) Callable 接口創(chuàng)建的多線程,我叫Thread-0返回值為:15
注:FutureTask 類提供了一個 get() 方法用來獲取 call() 方法的返回值,但需要注意的是調(diào)用這個方法會導(dǎo)致程序阻塞,必須要等到線程結(jié)束后才會得到返回值。
4、線程池(下面講)
三、線程池的創(chuàng)建使用(五種)
上面講的是通過new Thread等方式創(chuàng)建線程,這種方式的弊端是:
a. 每次new Thread新建對象性能差。
b. 線程缺乏統(tǒng)一管理,可能無限制新建線程,相互之間競爭,及可能占用過多系統(tǒng)資源導(dǎo)致死機或oom。
c. 缺乏更多功能,如定時執(zhí)行、定期執(zhí)行、線程中斷。
下面將要介紹的是Jdk提供的四種線程池的好處在于:
a. 重用存在的線程,減少對象創(chuàng)建、消亡的開銷,性能佳。
b. 可有效控制最大并發(fā)線程數(shù),提高系統(tǒng)資源的使用率,同時避免過多資源競爭,避免堵塞。
c. 提供定時執(zhí)行、定期執(zhí)行、單線程、并發(fā)數(shù)控制等功能。
1、newFixedThreadPool定長線程池
Executors.newFixedThreadPool:創(chuàng)建一個固定大小的線程池,可控制并發(fā)的線程數(shù),超出的線程會在隊列中等待。
import java.util.concurrent.*;public class Test { public static void main(String[] args) { ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2); for (int j = 0; j { for (int i = 0; i < 2; i++) { System.out.println("線程名:" + Thread.currentThread().getName() + " i是:" + i); } }); } fixedThreadPool.shutdown();//關(guān)閉線程池 //shutdownNow();//停止接收新任務(wù),原來的任務(wù)停止執(zhí)行,但是它并不對正在執(zhí)行的任務(wù)做任何保證,有可能它們都會停止,也有可能執(zhí)行完成。 }}
輸出:
線程名:pool-1-thread-1 i是:0線程名:pool-1-thread-1 i是:1線程名:pool-1-thread-2 i是:0線程名:pool-1-thread-1 i是:0線程名:pool-1-thread-1 i是:1線程名:pool-1-thread-2 i是:1
2、CachedThreadPool可緩存線程池
Executors.newCachedThreadPool:創(chuàng)建一個可緩存的線程池,若線程數(shù)超過處理所需,緩存一段時間后會回收,若線程數(shù)不夠,則新建線程。
可緩存線程池為無限大,當(dāng)執(zhí)行第二個任務(wù)時第一個任務(wù)已經(jīng)完成,會回收復(fù)用第一個任務(wù)的線程,而不用每次新建線程,可靈活回收空閑線程,若無可回收,則新建線程。
public class Test { public static void main(String[] args) { ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); for (int j = 0; j { for (int i = 0; i < 2; i++) { System.out.println("線程名:" + Thread.currentThread().getName() + " i是:" + i); } }); } cachedThreadPool.shutdown();//關(guān)閉線程池 }}
輸出:
線程名:pool-1-thread-1 i是:0線程名:pool-1-thread-1 i是:1線程名:pool-1-thread-3 i是:0線程名:pool-1-thread-2 i是:0線程名:pool-1-thread-2 i是:1線程名:pool-1-thread-3 i是:1
使用場景:
CachedThreadPool 是根據(jù)短時間的任務(wù)量來決定創(chuàng)建的線程數(shù)量的,所以它適合短時間內(nèi)有突發(fā)大量任務(wù)的處理場景。
3、newSingleThreadExecutor單線程化線程池
newSingleThreadExecutor線程池你可以理解為特殊的newFixedThreadPool線程池,它只會創(chuàng)建一個線程,并且所有任務(wù)按照指定順序。如果你創(chuàng)建了多個任務(wù),因為只會有一個線程,多余的任務(wù)會被阻塞到隊列里依次執(zhí)行。
下面的示例循環(huán)3次,每次都是用的一個線程,這個線程會先執(zhí)行第一個循環(huán)的任務(wù),在執(zhí)行第二個循環(huán)的任務(wù),再執(zhí)行第三個循環(huán)的任務(wù),所以輸出的 i 是有序的。
public class newSingleThreadExecutor { public static void main(String[] args) { ExecutorService singleThreadPool = Executors.newSingleThreadExecutor(); for (int j = 0; j { for (int i = 0; i < 3; i++) { System.out.println("線程名:" + Thread.currentThread().getName() + " i是:" + i); } }); } System.out.println("準(zhǔn)備關(guān)閉線程池"); singleThreadPool.shutdown();//關(guān)閉線程池 } }
輸出:
準(zhǔn)備關(guān)閉線程池線程名:pool-1-thread-1 i是:0線程名:pool-1-thread-1 i是:1線程名:pool-1-thread-1 i是:2線程名:pool-1-thread-1 i是:0線程名:pool-1-thread-1 i是:1線程名:pool-1-thread-1 i是:2線程名:pool-1-thread-1 i是:0線程名:pool-1-thread-1 i是:1線程名:pool-1-thread-1 i是:2
單個線程的線程池有什么意義?
單個線程的線程池相比于線程來說,它的優(yōu)點有以下 2 個:
可以復(fù)用線程:即使是單個線程池,也可以復(fù)用線程。
提供了任務(wù)管理功能:單個線程池也擁有任務(wù)隊列,在任務(wù)隊列可以存儲多個任務(wù),這是線程無法實現(xiàn)的,并且當(dāng)任務(wù)隊列滿了之后,可以執(zhí)行拒絕策略,這些都是線程不具備的。
4、newScheduledThreadPool周期性線程池
周期性線程池用來處理延時任務(wù)或定時任務(wù)。
無返回值的周期性線程池
public class newScheduledThreadPool { public static void main(String[] args) { ScheduledExecutorService scheduleThreadPool = Executors.newScheduledThreadPool(3); System.out.println(“測試1”); for (int i = 0; i { System.out.println(“線程名:” + Thread.currentThread().getName() + “已經(jīng)過了3秒”); }, 3, TimeUnit.SECONDS); } System.out.println(“測試2”); scheduleThreadPool.shutdown();//關(guān)閉線程池 } }
說明:
我們聲明了3個線程,創(chuàng)建的時候用循環(huán)創(chuàng)建了5個,多出來的2個會阻塞直到前3個線程有執(zhí)行完的再復(fù)用他們的線程;因為采用了延時3秒輸出,所以會先輸出測試1、測試2,然后等待3秒后再執(zhí)行輸出線程的內(nèi)容。
輸出:
測試1測試2線程名:pool-1-thread-2已經(jīng)過了3秒線程名:pool-1-thread-3已經(jīng)過了3秒線程名:pool-1-thread-1已經(jīng)過了3秒線程名:pool-1-thread-3已經(jīng)過了3秒線程名:pool-1-thread-2已經(jīng)過了3秒
有返回值的周期性線程池
public static void main(String[] args) { ScheduledExecutorService scheduleThreadPool = Executors.newScheduledThreadPool(3); System.out.println(“測試1”); ScheduledFuture scheduledFuture = scheduleThreadPool.schedule(() -> { return “線程名:” + Thread.currentThread().getName() + “已經(jīng)過了3秒”; }, 3, TimeUnit.SECONDS); System.out.println(“測試2”); try { //獲取線程返回的值并輸出 System.out.println(scheduledFuture.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } scheduleThreadPool.shutdown();//關(guān)閉線程池 } }
輸出:
測試1測試2線程名:pool-1-thread-1已經(jīng)過了3秒
定時線程執(zhí)行
定時執(zhí)行可以用scheduleAtFixedRate方法進(jìn)行操作,里面的參數(shù)4表示代碼或啟動運行后第4秒開始執(zhí)行,3表示每3秒執(zhí)行一次。因為我們設(shè)置了3個線程,所以運行后線程會在第4秒開始用3個線程每3秒執(zhí)行一次。
public static void main(String[] args) { ScheduledExecutorService scheduleThreadPool = Executors.newScheduledThreadPool(3); System.out.println(“測試1”); scheduleThreadPool.scheduleAtFixedRate(() -> { System.out.println(“線程名:” + Thread.currentThread().getName() + “已經(jīng)過了3秒”); }, 4, 3, TimeUnit.SECONDS); System.out.println(“測試2”); }
輸出:
測試1測試2線程名:pool-1-thread-1已經(jīng)過了3秒線程名:pool-1-thread-1已經(jīng)過了3秒線程名:pool-1-thread-2已經(jīng)過了3秒線程名:pool-1-thread-3已經(jīng)過了3秒……
5、ThreadPoolExecutor(手動創(chuàng)建線程池)
上面我們介紹了四種JDK自帶的線程池,但是平常不推薦使用。
ThreadPoolExecutor 相比于其他創(chuàng)建線程池的優(yōu)勢在于,它可以通過參數(shù)來控制最大任務(wù)數(shù)和拒絕策略,讓線程池的執(zhí)行更加透明和可控,所以在阿里巴巴《Java開發(fā)手冊》是這樣規(guī)定的:
【強制要求】線程池不允許使用 Executors 去創(chuàng)建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學(xué)更加明確線程池的運行規(guī)則,規(guī)避資源耗盡的風(fēng)險。
這一方面是由于jdk中自帶的線程池,都有其局限性,不夠靈活;另外使用ThreadPoolExecutor有助于大家明確線程池的運行規(guī)則,創(chuàng)建符合自己的業(yè)務(wù)場景需要的線程池,避免資源耗盡的風(fēng)險。
需要進(jìn)行線程池的初始化,所以引入以下依賴:
com.google.guava guava 29.0-jre
在開始前需要注意線程池的幾個參數(shù):
(在下面代碼的ThreadPoolExecutor里你會看到這些參數(shù)):
corePoolSize=> 線程池里的核心線程數(shù)量maximumPoolSize=> 線程池里允許有的最大線程數(shù)量keepAliveTime=> 空閑線程存活時間unit=> keepAliveTime的時間單位,比如分鐘,小時等workQueue=> 緩沖隊列threadFactory=> 線程工廠用來創(chuàng)建新的線程放入線程池handler=> 線程池拒絕任務(wù)的處理策略,比如拋出異常等策略線程池按以下行為執(zhí)行任務(wù) 1. 當(dāng)線程數(shù)小于核心線程數(shù)時,創(chuàng)建線程。 2. 當(dāng)線程數(shù)大于等于核心線程數(shù),且任務(wù)隊列未滿時,將任務(wù)放入任務(wù)隊列。 3. 當(dāng)線程數(shù)大于等于核心線程數(shù),且任務(wù)隊列已滿 -1 若線程數(shù)小于最大線程數(shù),創(chuàng)建線程 -2 若線程數(shù)等于最大線程數(shù),拋出異常,拒絕任務(wù)
無返回值的線程創(chuàng)建
代碼初始化了線程池并用 executorService.execute 分別創(chuàng)建了兩個線程,一個用來輸出本線程的名字,另一個用來異步調(diào)用 printA() 方法。
public static void main(String[] args) { System.out.println(“開始”); //線程池的初始化 ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat(“demo-pool-%d”).build(); ExecutorService executorService = new ThreadPoolExecutor( 60, 100, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy()); //開啟一個新線程用來輸出線程的名字 executorService.execute(() -> System.out.println(“第1個線程名字” + Thread.currentThread().getName())); //再開啟一個新線執(zhí)行printA() executorService.execute(() -> { System.out.println(“第2個線程名字” + Thread.currentThread().getName()); printA(); }); System.out.println(“完成”); executorService.shutdown(); } public static void printA() { for (int i = 0; i < 3; i++) { System.out.println("打?。篴aaaaaaaaaaaa"); } }
輸出:
開始完成第1個線程名字demo-pool-0第2個線程名字demo-pool-1打?。篴aaaaaaaaaaaa打?。篴aaaaaaaaaaaa打印:aaaaaaaaaaaaa
有返回值的多線程調(diào)用
使用submit
public static void main(String[] args) { System.out.println(“開始”); //線程池的初始化 ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat(“demo-pool-%d”).build(); ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy()); //異步調(diào)用對象integerCallableTask中的call()計算1-100的和 Future future = threadPoolExecutor.submit(() -> { int nummber = 100; int sum = 0; for (int i = 0; i <= nummber; i++) { sum += i; } return sum; }); try { //獲取計算的結(jié)果 Integer result = future.get(); System.out.println("和是:" + result); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println("完成"); //shutdown():停止接收新任務(wù),原來的任務(wù)繼續(xù)執(zhí)行 //shutdownNow():停止接收新任務(wù),原來的任務(wù)停止執(zhí)行 threadPoolExecutor.shutdown(); }
輸出:
開始和是:5050完成
線程池的submit和execute方法區(qū)別
1、接收的參數(shù)不一樣
execute接收的參數(shù)是new Runnable(),重寫run()方法,是沒有返回值的:
源碼:
public interface Executor { /** * Executes the given command at some time in the future. The command * may execute in a new thread, in a pooled thread, or in the calling * thread, at the discretion of the {@code Executor} implementation. * * @param command the runnable task * @throws RejectedExecutionException if this task cannot be * accepted for execution * @throws NullPointerException if command is null */ void execute(Runnable command);}
submit接收的參數(shù)是Callable,重寫call()方法,是有返回值的:
源碼:
/** * @throws RejectedExecutionException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */ public Future submit(Callable task) { if (task == null) throw new NullPointerException(); RunnableFuture ftask = newTaskFor(task); execute(ftask); return ftask; }