Java多线程的11种创建方式以及纠正网上流传很久的一个谬误
創建線程比較傳統的方式是繼承Thread類和實現Runnable,也可以用內部類,Lambda表達式,線程池,FutureTask等。
經常面試會問到繼承Thread類和實現Runnable的區別,然后網上會流傳如下這樣的說法,這是錯誤的。
流傳很久的錯誤說法:
這個說法是舉一個火車票售票的例子,大致意思是說實現Runnable接口可以實現多繼承,這一點說的是正確的,但是錯誤的是下面的,那些例子會說實現Runnable接口的多線程可以實現共享,而繼承Thread類的線程是不會共享的。其實之所以造成他們的說法看起來對是刻意在繼承Thread類的時候故意新建幾個線程,而成員變量又不是靜態的自然是不能共享的。
下面就是那個流傳很廣的關于繼承Thread類和實現Runnable的兩種方式的“區別”的來源:
參考類似此文《創建線程的兩種方式比較Thread VS Runnable》
說明:上面截圖中的是片面的,然后很多培訓機構和很多博主還引用這個,所以有必要澄清下這個問題。
正確的說法是:繼承Thread類和實現Runnable的最本質的區別是繼承接口可以實現多繼承。繼承Thread類的一樣可以實現共享。
?
先看Thread類和Runnable接口的關系
可以看到?Runnable接口就一個run方法,然后Thread類實現了這個run方法,同時自己又實現了很多其他方法。
1.繼承Thread類和重寫run()方法
public class MyThread extends Thread{private String name;private int i = 0;public MyThread(String name){this.name = name;}@Overridepublic void run() {i++;System.out.println(name+" i計數為:"+i+" "+Thread.currentThread().getName());}
}
public class MyThreadTest {public static void main(String[] args) {MyThread mt1 = new MyThread("線程");Thread t1 = new Thread(mt1);Thread t2 = new Thread(mt1);Thread t3 = new Thread(mt1);t1.start();t2.start();t3.start();}
}
?
說明:這樣就實現了所謂繼承的方式不能多個線程處理同一個資源的情況。
你可以自定義線程的name,這樣就不會是Thread-0,1,2,3這樣的系統生成的name了。
?
再修改代碼:
public class MyThreadTest {public static void main(String[] args) {MyThread t1 = new MyThread("線程1");MyThread t2 = new MyThread("線程2");MyThread t3 = new MyThread("線程3");t1.start();t2.start();t3.start();}
}
這次每個線程之間就不會共享數據,也就是那些謬誤中的用法。
?
2.實現Runnable接口
public class MyThreadRunnable implements Runnable{private String name;private int i = 0;public MyThreadRunnable(String name){this.name = name;}@Overridepublic void run() {i++;System.out.println(name+" i計數為:"+i+" "+Thread.currentThread().getName());}
}
public class MyThreadTest {public static void main(String[] args) {//MyThread t1 = new MyThread("線程1");//MyThread t2 = new MyThread("線程2");//MyThread t3 = new MyThread("線程3");MyThreadRunnable mt=new MyThreadRunnable("線程1");Thread t1 = new Thread(mt);Thread t2 = new Thread(mt);Thread t3 = new Thread(mt);t1.start();t2.start();t3.start();}
}
實現Runnable接口可以共享變量。
?
再把代碼改一改:
public class MyThreadTest {public static void main(String[] args) {//MyThread t1 = new MyThread("線程1");//MyThread t2 = new MyThread("線程2");//MyThread t3 = new MyThread("線程3");MyThreadRunnable mt1=new MyThreadRunnable("線程1");MyThreadRunnable mt2=new MyThreadRunnable("線程2");MyThreadRunnable mt3=new MyThreadRunnable("線程3");Thread t1 = new Thread(mt1);Thread t2 = new Thread(mt2);Thread t3 = new Thread(mt3);t1.start();t2.start();t3.start();}
}
這次就不會共享變量了。
所以這樣就證明了共享變量和繼承Thread類和實現Runnable并無關系!
?
3.實現線程創建的兩種簡潔方式(匿名內部類+Lambda表達式)
這種方式適合于創建啟動線程次數較少的環境,一般書寫更加簡便。
//方式1:相當于繼承了Thread類,作為子類重寫run()實現new Thread() {public void run() {System.out.println("匿名內部類創建線程方式1..." + Thread.currentThread().getName());}}.start();//方式2:實現Runnable,Runnable作為匿名內部類new Thread(new Runnable() {public void run() {System.out.println("匿名內部類創建線程方式2..." + Thread.currentThread().getName());}}).start();//方式3:Lambda表達式創建線程new Thread(() -> {System.out.println("Lambda表達式創建線程方式..." + Thread.currentThread().getName());}).start();
?
如果需要自定義線程名字可以修改如下:
//方式3:Lambda表達式創建線程new Thread(() -> {System.out.println("Lambda表達式創建線程方式..." + Thread.currentThread().getName());},"線程1").start();
?
4.實現線程的線程池方式
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ThreadPool {public static void main(String[] args) {ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);for (int i = 0; i < 10; i++) {final int index = i;fixedThreadPool.execute(new Runnable() {public void run() {try {System.out.println(index+ " "+Thread.currentThread().getName()+" "+DateUtil.getNowTimeString());Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}});}fixedThreadPool.shutdown();}
}
這個帖子寫的很好,還有其他幾種模式: 《java常用的幾種線程池比較》
newCachedThreadPool
創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
newFixedThreadPool
創建一個指定工作線程數量的線程池。每當提交一個任務就創建一個工作線程,如果工作線程數量達到線程池初始的最大數,則將提交的任務存入到池隊列中。
FixedThreadPool是一個典型且優秀的線程池,它具有線程池提高程序效率和節省創建線程時所耗的開銷的優點。但是,在線程池空閑時,即線程池中沒有可運行任務時,它不會釋放工作線程,還會占用一定的系統資源。
newSingleThreadExecutor
創建一個單線程化的Executor,即只創建唯一的工作者線程來執行任務,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO,?優先級)執行。如果這個線程異常結束,會有另一個取代它,保證順序執行。單工作線程最大的特點是可保證順序地執行各個任務,并且在任意給定的時間不會有多個線程是活動的。
newScheduleThreadPool
創建一個定長的線程池,而且支持定時的以及周期性的任務執行,支持定時及周期性任務執行。
5.實現異步的Future,FutureTask,CompletableFuture方式
本質還是線程,因為Java目前語言層面沒有協程,需要三方類庫或者修改JVM才可以,
參考本人另一帖:《異步編程原理以及Java實現》,具體源碼分析都有本質還是調用class Executors,只不過是這種方式可以回調而已。原因是繼承和實現了Runnable接口這2種方式創建線程都有一個缺陷就是:在執行完任務之后無法獲取執行結果。
自從Java 1.5開始,就提供了Callable和Future,通過它們可以在任務執行完畢之后得到任務執行結果。
雖然 Future 以及相關使用方法提供了異步執行任務的能力,但是對于結果的獲取卻是很不方便,只能通過阻塞或者輪詢的方式得到任務的結果。阻塞的方式顯然和我們的異步編程的初衷相違背,輪詢的方式又會耗費無謂的 CPU 資源,而且也不能及時地得到計算結果。
在Java8中,CompletableFuture提供了非常強大的Future的擴展功能,可以幫助我們簡化異步編程的復雜性,并且提供了函數式編程的能力,可以通過回調的方式處理計算結果,也提供了轉換和組合 CompletableFuture 的方法。
《CompletableFuture基本用法》
6.定時器方式
這種方式不是為了實現線程,但是他確實是起了一個線程
import java.util.Timer;
import java.util.TimerTask;public class TimerTest {public static void main(String[] args) {Timer timer = new Timer();timer.schedule(newTimerTask() {@Overridepublic void run() {System.out.println("定時任務延遲0(即立刻執行),每隔1000ms執行一次"+ " "+Thread.currentThread().getName()+" "+ DateUtil.getNowTimeString());}}, 0, 1000);}
}
public abstract class TimerTask implements Runnable
?
?從類圖可以看出TimerTask實現了Runnable接口的run()方法。
詳細分析可以參考《Java 定時器 Timer 源碼分析和使用建議》:
Timer 可以按計劃執行重復的任務或者定時執行指定任務,這是因為 Timer 內部利用了一個后臺線程 TimerThread 有計劃地執行指定任務。
-
Timer:是一個實用工具類,該類用來調度一個線程(schedule a thread),使它可以在將來某一時刻執行。 Java 的 Timer 類可以調度一個任務運行一次或定期循環運行。 Timer tasks should complete quickly. 即定時器中的操作要盡可能花費短的時間。
-
TimerTask:一個抽象類,它實現了 Runnable 接口。我們需要擴展該類以便創建自己的 TimerTask ,這個 TimerTask 可以被 Timer 調度。
一個 Timer 對象對應的是單個后臺線程,其內部維護了一個 TaskQueue,用于順序執行計時器任務 TimeTask 。
?
7.Spring異步任務支持
@EnableAsync和@Async開始異步任務支持
Spring通過任務執行器(TaskExecutor)來實現多線程和并發編程。使用ThreadPoolTaskExecutor可實現一個基于線程池的TaskExecutor.在開發中實現異步任務,我們可以在配置類中添加@EnableAsync開始對異步任務的支持,并在相應的方法中使用@Async注解來聲明一個異步任務。?
詳細參考《@EnableAsync和@Async開始異步任務支持》
?
8.可以用并行流創建線程
import java.util.Arrays;
import java.util.stream.Collectors;public class StreamParallel {public static void main(String args[]) {for (int i = 0; i < 1000000; i++) {Arrays.asList(1, 2, 3, 4, 5, 6, 7, 9, 8, 0, 1).stream().parallel().collect(Collectors.groupingBy(x -> x % 10)).forEach((x, y) -> System.out.println(x + ":" + y));}}
}
?可以看到默認的parallel計算啟動了三個線程進行并行。
參考《Java8 Stream 并行計算實現的原理》
Java 并發編程:線程間的協作(wait/notify/sleep/yield/join)
總結
以上是生活随笔為你收集整理的Java多线程的11种创建方式以及纠正网上流传很久的一个谬误的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 3万块钱存银行一年利息多少?
- 下一篇: 支撑Java框架的基础技术:泛型,反射,