文章目錄
- 多線程
- -1 高并發
- 〇、使用多線程的場景
- 1. 為什么使用多線程
- 1. 線程概述
- 1.1 線程和進程
- 1.2 并發和并行
- 1.3 多線程的優勢
- 1.4 程序運行原理
- 1.5 主線程
- 1.6 線程的 6 種狀態
- 2. 線程的創建和啟動
- 2.1 Thread類
- 2.2創建線程有哪幾種方法
- 2.2.1 繼承**Thread**類,重寫**Run**方法(其中**Thread**類本身也是實現了**Runnable**接口)
- 2.2.2 實現**Runnable**接口,重寫**run**方法
- 2.2.3 實現 **Callable** 接口,重寫 **call**方法(有返回值)
- 2.2.4 通過線程池創建線程
- 4 線程池的核心參數有哪些:
- 4個參數的設計:
來談談多線程,線程的創建,
多線程
-1 高并發
參考文章: 【多線程高并發編程】二 實現多線程的幾種方式
〇、使用多線程的場景
1. 為什么使用多線程
通俗的解釋一下多線程先:
多線程用于堆積處理,就像一個大土堆,一個推土機很慢,那么10個推土機一起來處理,當然速度就快了,不過由于位置的限制,如果20個推土機,那么推土機之間會產生相互的避讓,相互摩擦,相互擁擠,反而不如10個處理的好,所以,多線程處理,線程數要開的恰當,就可以提高效率。
多線程使用的目的:
1、吞吐量:做WEB,容器幫你做了多線程,但是它只能幫你做請求層面的,簡單的說,就是一個請求一個線程(如struts2,是多線程的,每個客戶端請求創建一個實例,保證線程安全),或多個請求一個線程,如果是單線程,那只能是處理一個用戶的請求。
2、伸縮性:通過增加CPU核數來提升性能。
多線程的使用場景:
1、常見的瀏覽器、Web服務(現在寫的web是中間件幫你完成了線程的控制),web處理請求,各種專用服務器(如游戲服務器)
2、servlet多線程
3、FTP下載,多線程操作文件
4、數據庫用到的多線程
5、分布式計算
6、tomcat,tomcat內部采用多線程,上百個客戶端訪問同一個WEB應用,tomcat接入后就是把后續的處理扔給一個新的線程來處理,這個新的線程最后調用我們的servlet程序,比如doGet或者dpPost方法
7、后臺任務:如定時向大量(100W以上)的用戶發送郵件;定期更新配置文件、任務調度(如quartz),一些監控用于定期信息采集
8、自動作業處理:比如定期備份日志、定期備份數據庫
9、異步處理:如發微博、記錄日志
10、頁面異步處理:比如大批量數據的核對工作(有10萬個手機號碼,核對哪些是已有用戶)
11、數據庫的數據分析(待分析的數據太多),數據遷移
12、多步驟的任務處理,可根據步驟特征選用不同個數和特征的線程來協作處理,多任務的分割,由一個主線程分割給多個線程完成
1. 線程概述
1.1 線程和進程
? 進程是處于運行過程中的程序,并且具有一定的獨立功能,進程是系統進行資源分配和調度的一個獨立單位。
線程也被稱為輕量級進程,線程是進程的組成部分,一個進程可以擁有多個線程,一個線程必須有一個父進程。線程可以擁有自己的堆棧、自己的程序計數器和自己的局部變量,但不擁有系統資源,它與父進程的其它線程共享該進程所擁有的全部資源。一個線程可以創建和撤銷另一個線程,同一個進程中的多個線程之間可以并發執行。
1.2 并發和并行
? 并行指在同一時刻,有多條指令在多個處理器上同時執行;并發指在同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,使得在宏觀上具有多個進程同時執行的效果。
1.3 多線程的優勢
(1)進程之間不能共享內存,但線程之間共享內存卻非常容易。
(2)系統創建進程時需要為該進程重新分配系統資源,但創建線程代價小得多,因此使用多線程來實現多任務并發比多進程的效率高。
(3)java語言內置了多線程功能支持,而不是單純地作為底層操作系統的調度方式,從而簡化了java的多線程編程。
1.4 程序運行原理
? 分時調度:
所有線程輪流使用 CPU 的使用權,平均分配每個線程占用 CPU 的時間。
? 搶占式調度:
優先讓優先級高的線程使用 CPU,如果線程的優先級相同,那么會隨機選擇一個(線程隨機性),Java使用的為搶占式調度。
1.5 主線程
? jvm啟動后,必然有一個執行路徑(線程)從main方法開始的,一直執行到main方法結束,這個線程在java中稱之為主線程。當程序的主線程執行時,如果遇到了循環而導致程序在指定位置停留時間過長,則無法馬上執行下面的程序,需要等待循環結束后能夠執行。
1.6 線程的 6 種狀態
就像生物從出生到長大、最終死亡的過程一樣,線程也有自己的生命周期,在 Java 中線程的生命周期中一共有 6 種狀態。
-
New(新創建)
-
Runnable(可運行)
-
Blocked(被阻塞)
-
Waiting(等待)
-
Timed Waiting(計時等待)
-
Terminated(被終止)
如果想要確定線程當前的狀態,可以通過 getState() 方法,并且線程在任何時刻只可能處于 1 種狀態。
運行狀態可能會有阻塞:
2. 線程的創建和啟動
2.1 Thread類
Java使用Thread類代表線程,所有的線程對象都必須是Thread類或其子類的實例。每個線程的作用是完成一定的任務,實際上就是執行一段程序流。Java使用縣城執行體來表示這段流。
2.2創建線程有哪幾種方法
2.2.1 繼承Thread類,重寫Run方法(其中Thread類本身也是實現了Runnable接口)
(1)定義Thread類的子類,并重寫該類的run方法,該run方法的方法體就代表了線程要完成的任務。因此把run()方法稱為執行體。
(2)創建Thread子類的實例,即創建了線程對象。
(3)調用線程對象的start()方法來啟動該線程。
示例代碼:
package com.thread;
public class FirstThreadTest extends Thread{int i = 0;//重寫run方法,run方法的方法體就是現場執行體public void run(){for(;i<100;i++){System.out.println(getName()+" "+i);}}public static void main(String[] args){for(int i = 0;i< 100;i++){System.out.println(Thread.currentThread().getName()+" : "+i);if(i==20){new FirstThreadTest().start();new FirstThreadTest().start();}}}
}
2.2.2 實現Runnable接口,重寫run方法
(1)定義runnable接口的實現類,并重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。
(2)創建 Runnable實現類的實例,并依此實例作為Thread的target來創建Thread對象,該Thread對象才是真正的線程對象。
(3)調用線程對象的start()方法來啟動該線程。
示例代碼:
public class RunnableThreadTest implements Runnable{private int i;public void run() {for(i = 0;i <100;i++){System.out.println(Thread.currentThread().getName()+" "+i);}}public static void main(String[] args){for(int i = 0;i < 100;i++) {System.out.println(Thread.currentThread().getName()+" "+i);if(i==20) {RunnableThreadTest rtt = new RunnableThreadTest();new Thread(rtt,"新線程1").start();new Thread(rtt,"新線程2").start();}}}
}
2.2.3 實現 Callable 接口,重寫 call方法(有返回值)
通過Callable和Future創建線程
(1)創建Callable接口的實現類,并實現**call()方法,該call()**方法將作為線程執行體,并且有返回值。
(2)創建Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的**call()**方法的返回值。
(3)使用FutureTask對象作為Thread對象的target創建并啟動新線程。
(4)調用FutureTask對象的get()方法來獲得子線程執行結束后的返回值
實例代碼:
package com.thread;import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class CallableThreadTest implements Callable<Integer> {public static void main(String[] args) {CallableThreadTest ctt = new CallableThreadTest();FutureTask<Integer> ft = new FutureTask<>(ctt);for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + " 的循環變量i的值" + i);if (i == 20) {new Thread(ft, "有返回值的線程").start();}}try {System.out.println("子線程的返回值:" + ft.get());} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}@Overridepublic Integer call() throws Exception {int i = 0;for (; i < 100; i++) {System.out.println(Thread.currentThread().getName() + " " + i);}return i;}}
2.2.4 通過線程池創建線程
創建線程的三種方式的對比
- 采用實現Runnable、Callable接口的方式創見多線程時
優勢:
線程類只是實現了Runnable接口或Callable接口,還可以繼承其他類。
在這種方式下,多個線程可以共享同一個target對象,所以非常適合多個相同線程來處理同一份資源的情況,從而可以將CPU、代碼和數據分開,形成清晰的模型,較好地體現了面向對象的思想。
劣勢:
編程稍微復雜,如果要訪問當前線程,則必須使用Thread.currentThread()方法。 - 使用繼承Thread類的方式創建多線程時
優勢:
編寫簡單,如果需要訪問當前線程,則無需使用Thread.currentThread()方法,直接使用this即可獲得當前線程。
劣勢:
線程類已經繼承了Thread類,所以不能再繼承其他父類。
4 線程池的核心參數有哪些:
為什么使用線程池?
使用線程池最大的原因就是可以根據系統的需求和硬件環境靈活的控制線程的數量,且可以對所有線程進行統一的管理和控制,從而提高系統的運行效率,降低系統運行運行壓力;當然了,使用線程池的原因不僅僅只有這些,我們可以從線程池自身的優點上來進一步了解線程池的好處;
使用線程池的優勢有哪些?
- 線程和任務分離,提升線程重用性;
- 控制線程并發數量,降低服務器壓力,統一管理所有線程;
- 提升系統響應速度,假如創建線程用的時間為T1,執行任務用的時間為T2,銷毀線程用的時間為T3,那么使用線程池就免去了T1和T3的時間;
構造方法:
public ThreadPoolExecutor(int corePoolSize, //核心線程數量int maximumPoolSize,// 最大線程數long keepAliveTime, // 最大空閑時間TimeUnit unit, // 時間單位BlockingQueue<Runnable> workQueue, // 任務隊列ThreadFactory threadFactory, // 線程工廠RejectedExecutionHandler handler // 飽和處理機制)
{ ... }
4個參數的設計:
1:核心線程數(corePoolSize)
核心線程數的設計需要依據任務的處理時間和每秒產生的任務數量來確定,例如:執行一個任務需要0.1秒,系統百分之80的時間每秒都會產生100個任務,那么要想在1秒內處理完這100個任務,就需要10個線程,此時我們就可以設計核心線程數為10;當然實際情況不可能這么平均,所以我們一般按照8020原則設計即可,既按照百分之80的情況設計核心線程數,剩下的百分之20可以利用最大線程數處理;
2:任務隊列長度(workQueue)
任務隊列長度一般設計為:核心線程數/單個任務執行時間*2即可;例如上面的場景中,核心線程數設計為10,單個任務執行時間為0.1秒,則隊列長度可以設計為200;
3:最大線程數(maximumPoolSize)
最大線程數的設計除了需要參照核心線程數的條件外,還需要參照系統每秒產生的最大任務數決定:例如:上述環境中,如果系統每秒最大產生的任務是1000個,那么,最大線程數=(最大任務數-任務隊列長度)*單個任務執行時間;既: 最大線程數=(1000-200)*0.1=80個;
4:最大空閑時間(keepAliveTime)
這個參數的設計完全參考系統運行環境和硬件壓力設定,沒有固定的參考值,用戶可以根據經驗和系統產生任務的時間間隔合理設置一個值即可;