一篇學會Java多線程之線程
本文轉載自微信公眾號「我是開發者FTD」,作者FTD。轉載本文請聯系我是開發者FTD公眾號。
我們在學習軟件開發時,多線程,高并發是一個必不可少的知識點,也是在面試時必會問到的內容,為了讓大家對多線程,高并發編程有個清晰認識,特地組織了一個專欄來專門介紹一下,希望能對大家有一些幫助。
線程簡介
線程是程序運行的基本執行單元。當操作系統(不包括單線程的操作系統,如微軟早期的DOS)在執行一個程序時,會在系統中建立一個進程,而在這個進程中,必須至少建立一個線程(這個線程被稱為主線程)來作為這個程序運行的入口點。因此,在操作系統中運行的任何程序都至少有一個主線程。
進程和線程是現代操作系統中兩個必不可少的運行模型。在操作系統中可以有多個進程,這些進程包括系統進程(由操作系統內部建立的進程)和用戶進程(由用戶程序建立的進程);一個進程中可以有一個或多個線程。進程和進程之間不共享內存,也就是說系統中的進程是在各自獨立的內存空間中運行的。而一個進程中的線可以共享系統分派給這個進程的內存空間。
在進一步介紹之前,我們先來了解一些基本概念,以幫助大家更快速的理解。
基本概念
進程
進程是操作系統中正在執行的不同的應用程序,例如:我們可以同時打開微信和QQ,甚至更多的程序。
在操作系統中運行的程序就是進程,進程就是執行程序的一次執行過程,它是一個動態的概念式系統資源分配的單位
通常在一個進程中可以包含若干個線程,當然一個進程中至少有一個線程,不然沒有存在的意義,線程是CPU調度和執行的單位
線程
線程是一個應用程序進程中不同的執行路徑,例如:我們的WEB服務器,能夠為多個用戶同時提供請求服務。
進程是不活潑的,進程從來不執行任何東西,它只是線程的容器。線程總是在某個進程環境中創建的,而且它的整個生命周期都在該進程中。
在一個進程中,如果創建了多個線程,線程的運行是由調度器安排調度的,調度器是與操作系統緊密相關的,先后順序是不能人為干預的
多線程
多線程擁有多條執行路徑,「主線程與子線程并行交替執行」(普通方法只有主線程一條路徑),對同一份資源操作時,會存在資源搶奪的問題,這時就需要加入并發控制了。
一個Java應用程序,至少有三個線程: main()主線程, gc()垃圾回收線程,異常處理線程。當然如果發生異常,會影響主線程。
多線程程序的優點:
- 提高應用程序的響應。對圖形化界面更有意義,可增強用戶體驗。同時做多個事情。比如:一邊聽歌、一邊寫代碼。
- 提高計算機系統CPU的利用率。不過線程也會帶來額外的開銷,如CPU調度時間,并發控制帶來的系統開銷。
- 改善程序結構。將既長又復雜的進程分為多個線程,獨立運行,利于理解和修改。
何時需要多線程?
程序需要同時執行兩個或多個任務。
需要一些后臺運行的程序時,比如:Java后臺運行的GC線程。
創建線程
Java中創建線程有四種方式,我們下面依次介紹一下。
1、繼承 Thread 類
(1)定義Thread類的子類,并重寫該類的run方法,該run方法的方法體就代表了線程要完成的任務。因此把run()方法稱為執行體。
(2)創建Thread子類的實例對象,即創建了一個線程對象。
(3)調用該線程對象的start()方法來啟動該線程。
示例代碼:
- public class MyThread extends Thread {
- // 總票數
- public int count = 10;
- @Override
- public void run() {
- // 當還有票時就繼續售賣
- while (count > 0) {
- // 剩余票數
- count--;
- System.out.println(
- Thread.currentThread().getName() + "售賣第 " + (10 - count) + " 張票,當前剩余票數: " + count);
- }
- }
- public static void main(String[] args) {
- MyThread myThread = new MyThread();
- myThread.start();
- }
- }
2、實現Runnable接口
(1)定義Runnable接口的實現類,并重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。
(2)創建 Runnable實現類的實例對象,并將該實例作為Thread的target來創建一個Thread對象,該Thread對象才是真正的線程運行對象。
(3)調用該線程對象的start()方法來啟動線程。
示例代碼:
- public class MyRunableThread implements Runnable {
- // 總票數
- public int count = 10;
- @Override
- public void run() {
- // 當還有票時就繼續售賣
- while (count > 0) {
- // 剩余票數
- count--;
- System.out.println(
- Thread.currentThread().getName() + "售賣第 " + (10 - count) + " 張票,當前剩余票數: " + count);
- }
- }
- public static void main(String[] args) {
- MyRunableThread myRunableThread = new MyRunableThread();
- Thread myThread = new Thread(myRunableThread);
- myThread.start();
- }
- }
「Thread 和 Runnable 的區別」
上述兩種方法是大家最常見到的兩種創建線程的方法,也常常會被問到兩種方式創建線程的區別,下面簡單總結了一下:
「繼承 Thread 類」
子類繼承 Thread 類具備多線程能力
啟動線程:子類線程對象調用 .start()方法
不建議使用:避免 OOP 單繼承局限性
「實現接口 Runnable」
- 具有多線程能力
- 啟動線程:傳入目標對象 + Thread對象調用.start()方法
- 推薦使用:避免單繼承局限性,方便同一個對象被多個線程使用
另外,在使用線程池時只能放入實現Runable或Callable類線程,不能直接放入繼承Thread的類
3、實現Callable接口
(1)創建Callable接口的實現類,并實現call()方法,該call()方法將作為線程執行體,并且有返回值。
(2)創建Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值。
(3)使用FutureTask對象作為Thread對象的target創建并啟動新線程。
(4)調用FutureTask對象的get()方法來獲得子線程執行結束后的返回值。
示例代碼:
- public class MyCallableThread implements Callable<String> {
- // 總票數
- private int count = 10;
- @Override
- public String call() throws Exception {
- // 當還有票時就繼續售賣
- while (count > 0) {
- // 剩余票數
- count--;
- System.out.println(
- Thread.currentThread().getName() + "售賣第 " + (10 - count) + " 張票,當前剩余票數: " + count);
- }
- return "票已售完";
- }
- public static void main(String[] args) throws InterruptedException, ExecutionException {
- Callable<String> callable = new MyCallableThread();
- FutureTask<String> futureTask = new FutureTask<>(callable);
- Thread myThread = new Thread(futureTask);
- myThread.start();
- // 打印返回結果
- System.out.println(futureTask.get());
- }
- }
「Runnable和Callable的區別:」
Callable規定的方法是call(),Runnable規定的方法是run()。
Callable的任務執行后可返回值,而Runnable的任務是不能有返回值。
call方法可以拋出異常,run方法不可以。
4、線程池
Java默認提供了五種線程池,通過Executors創建,分別為:
- 「newCachedThreadPool」 創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
- 「newFixedThreadPool」 創建一個定長線程池,可控制線程最大并發數,超出的線程會在隊列中等待。
- 「newScheduledThreadPool」 創建一個定長線程池,支持定時及周期性任務執行。
- 「newSingleThreadExecutor」 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
- 「newWorkStealingPool」 創建一個具有搶占式操作的線程池,由于能夠合理的使用CPU進行對任務操作(并行操作),所以適合使用在很耗時的任務中。
示例代碼:
- public class MyThreadPool implements Runnable {
- // 總票數
- public int count = 10;
- @Override
- public void run() {
- // 當還有票時就繼續售賣
- while (count > 0) {
- // 剩余票數
- count--;
- System.out.println(
- Thread.currentThread().getName() + "售賣第 " + (10 - count) + " 張票,當前剩余票數: " + count);
- }
- }
- public static void main(String[] args) {
- ExecutorService ex = Executors.newFixedThreadPool(5);
- MyThreadPool t = new MyThreadPool();
- ex.submit(t);
- ex.shutdown();
- }
- }
關于線程池,后續會有單獨文章給大家詳細介紹。
通過以上的內容,希望大家可以對線程有個初步的認識,相關示例代碼,稍后整理后我會上傳到GitHub上,也請大家留意我們的后續文章。