網(wǎng)站如何做原創(chuàng)文章什么是關鍵詞舉例說明
目錄
- 線程的概述
- 多線程的創(chuàng)建
- 方式一:繼承Thread類
- 方式二:實現(xiàn)Runnable接口
- 方式三:利用Callable接口、FutureTask類來實現(xiàn)。
- Thread常用的方法
- 線程安全問題
- 線程安全問題概述
- 線程安全問題案例
- 取錢案例描述
- 模擬代碼如下:
- 執(zhí)行結果
- 線程同步
- 概述
- 線程同步的常見方案
- 1. 同步代碼塊
- 2. 同步方法
- 3. Lock鎖
- 線程通信
- 概述
- 線程通信案例
- 案例代碼實現(xiàn)
- 線程池
- 線程池概述
- 線程池創(chuàng)建
- 線程池執(zhí)行Runnable任務
- 代碼案例
- 線程池執(zhí)行Callable任務
- 代碼案例
- 核心線程數(shù)量到底應該配置多少呢?
- 線程池工具類(Executors)
- 并發(fā)、并行和生命周期
- 并發(fā)和并行
- 1. 什么是進程、線程?
- 2. 什么是并發(fā)?
- 3. 什么是并行?
- 4. 多線程到底是并發(fā)還是并行呢?
- 線程的生命周期
- 樂觀鎖與悲觀鎖
線程的概述
什么是線程?
線程(Thread)是一個程序內部的一條執(zhí)行流程。
程序中如果只有一條執(zhí)行流程,那這個程序就是單線程的程序
什么是多線程?
多線程是指從軟硬件上實現(xiàn)的多條執(zhí)行流程的技術(多條線程由CPU負責調度執(zhí)行)
如何在程序中創(chuàng)建出多條線程?
Java是通過java.lang.Thread 類的對象來代表線程的。
多線程的創(chuàng)建
方式一:繼承Thread類
- 定義一個子類MyThread繼承線程類java.lang.Thread,重寫run()方法
- 創(chuàng)建MyThread類的對象
- 調用線程對象的start()方法啟動線程(啟動后還是執(zhí)行run方法的)
示例代碼如下:
主線程類:
public class ThreadTest1 {// main方法是由一條默認的主線程負責執(zhí)行。public static void main(String[] args) {// 3、創(chuàng)建MyThread線程類的對象代表一個線程Thread t = new MyThread();// 4、啟動線程(自動執(zhí)行run方法的)t.start(); // main線程 t線程for (int i = 1; i <= 5; i++) {System.out.println("主線程main輸出:" + i);}}
}
子線程類:
/*** 1、讓子類繼承Thread線程類。*/
public class MyThread extends Thread{// 2、必須重寫Thread類的run方法@Overridepublic void run() {// 描述線程的執(zhí)行任務。for (int i = 1; i <= 5; i++) {System.out.println("子線程MyThread輸出:" + i);}}
}
方式一優(yōu)缺點:
優(yōu)點:編碼簡單
缺點:線程類已經(jīng)繼承Thread,無法繼承其他類,不利于功能的擴展。
多線程的注意事項
1、啟動線程必須是調用start方法,不是調用run方法。
直接調用run方法會當成普通方法執(zhí)行,此時相當于還是單線程執(zhí)行。
只有調用start方法才是啟動一個新的線程執(zhí)行。
2、不要把主線程任務放在啟動子線程之前。
這樣主線程一直是先跑完的,相當于是一個單線程的效果了。
方式二:實現(xiàn)Runnable接口
- 定義一個線程任務類MyRunnable實現(xiàn)Runnable接口,重寫run()方法
- 創(chuàng)建MyRunnable任務對象
- 把MyRunnable任務對象交給Thread處理。
- 調用線程對象的start()方法啟動線程
示例代碼如下:
定義一個任務類
/*** 1、定義一個任務類,實現(xiàn)Runnable接口*/
public class MyRunnable implements Runnable{// 2、重寫runnable的run方法@Overridepublic void run() {// 線程要執(zhí)行的任務。for (int i = 1; i <= 5; i++) {System.out.println("子線程輸出 ===》" + i);}}
}
主線程類
/*** 多線程的創(chuàng)建方式二:實現(xiàn)Runnable接口。*/
public class ThreadTest2 {public static void main(String[] args) {// 3、創(chuàng)建任務對象。Runnable target = new MyRunnable();// 4、把任務對象交給一個線程對象處理。// public Thread(Runnable target)new Thread(target).start();for (int i = 1; i <= 5; i++) {System.out.println("主線程main輸出 ===》" + i);}}
}
方式二的優(yōu)缺點
優(yōu)點:任務類只是實現(xiàn)接口,可以繼續(xù)繼承其他類、實現(xiàn)其他接口,擴展性強。
缺點:需要多一個Runnable對象。
線程創(chuàng)建方式二的匿名內部類寫法
- 可以創(chuàng)建Runnable的匿名內部類對象。
- 再交給Thread線程對象。
- 再調用線程對象的start()啟動線程。
代碼示例:
/*** 多線程創(chuàng)建方式二的匿名內部類寫法。*/
public class ThreadTest2_2 {public static void main(String[] args) {// 1、直接創(chuàng)建Runnable接口的匿名內部類形式(任務對象)Runnable target = new Runnable() {@Overridepublic void run() {for (int i = 1; i <= 5; i++) {System.out.println("子線程1輸出:" + i);}}};new Thread(target).start();// 簡化形式1:new Thread(new Runnable() {@Overridepublic void run() {for (int i = 1; i <= 5; i++) {System.out.println("子線程2輸出:" + i);}}}).start();// 簡化形式2:new Thread(() -> {for (int i = 1; i <= 5; i++) {System.out.println("子線程3輸出:" + i);}}).start();for (int i = 1; i <= 5; i++) {System.out.println("主線程main輸出:" + i);}}
}
方式三:利用Callable接口、FutureTask類來實現(xiàn)。
前兩種線程創(chuàng)建方式都存在的一個問題:
假如線程執(zhí)行完畢后有一些數(shù)據(jù)需要返回,他們重寫的run方法均不能直接返回結果。
怎么解決這個問題?
JDK 5.0提供了Callable接口和FutureTask類來實現(xiàn)(多線程的第三種創(chuàng)建方式)。
這種方式最大的優(yōu)點:可以返回線程執(zhí)行完畢后的結果
方式三 實現(xiàn)步驟:
- 創(chuàng)建任務對象
定義一個類實現(xiàn)Callable接口,重寫call方法,封裝要做的事情,和要返回的數(shù)據(jù)。
把Callable類型的對象封裝成FutureTask(線程任務對象)。 - 把線程任務對象交給Thread對象。
- 調用Thread對象的start方法啟動線程。
- 線程執(zhí)行完畢后、通過FutureTask對象的的get方法去獲取線程任務執(zhí)行的結果。
示例代碼如下:
創(chuàng)建任務對象類
import java.util.concurrent.Callable;/*** 1、讓這個類實現(xiàn)Callable接口*/
public class MyCallable implements Callable<String> {private int n;public MyCallable(int n) {this.n = n;}// 2、重寫call方法@Overridepublic String call() throws Exception {// 描述線程的任務,返回線程執(zhí)行返回后的結果。// 需求:求1-n的和返回。int sum = 0;for (int i = 1; i <= n; i++) {sum += i;}return "線程求出了1-" + n + "的和是:" + sum;}
}
主線程類
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;/*** 線程的創(chuàng)建方式三:實現(xiàn)Callable接口。*/
public class ThreadTest3 {public static void main(String[] args) throws Exception {// 3、創(chuàng)建一個Callable的對象Callable<String> call = new MyCallable(100);// 4、把Callable的對象封裝成一個FutureTask對象(任務對象)// 未來任務對象的作用?// 1、是一個任務對象,實現(xiàn)了Runnable對象.// 2、可以在線程執(zhí)行完畢之后,用未來任務對象調用get方法獲取線程執(zhí)行完畢后的結果。FutureTask<String> f1 = new FutureTask<>(call);// 5、把任務對象交給一個Thread對象new Thread(f1).start();Callable<String> call2 = new MyCallable(200);FutureTask<String> f2 = new FutureTask<>(call2);new Thread(f2).start();// 6、獲取線程執(zhí)行完畢后返回的結果。// 注意:如果執(zhí)行到這兒,假如上面的線程還沒有執(zhí)行完畢// 這里的代碼會暫停,等待上面線程執(zhí)行完畢后才會獲取結果。String rs = f1.get();System.out.println(rs);String rs2 = f2.get();System.out.println(rs2);}
}
FutureTask的API
方式三的優(yōu)缺點
優(yōu)點:線程任務類只是實現(xiàn)接口,可以繼續(xù)繼承類和實現(xiàn)接口,擴展性強;可以在線程執(zhí)行完畢后去獲取線程執(zhí)行的結果:
缺點:編碼復雜一點。線程創(chuàng)建方式三的優(yōu)缺點
優(yōu)點:線程任務類只是實現(xiàn)接口,可以繼續(xù)繼承類和實現(xiàn)接口,擴展性強;可以在線程執(zhí)行完畢后去獲取線程執(zhí)行的結果。
缺點:編碼復雜一點。
Thread常用的方法
Thread提供了很多與線程操作相關的方法
示例代碼:
子線程
public class MyThread extends Thread{public MyThread(String name){super(name); // 為當前線程設置名字了}@Overridepublic void run() {// 哪個線程執(zhí)行它,它就會得到哪個線程對象。Thread t = Thread.currentThread();for (int i = 1; i <= 3; i++) {System.out.println(t.getName() + "輸出:" + i);}}
}
主方法
/*** Thread的常用方法。*/
public class ThreadTest1 {public static void main(String[] args) {Thread t1 = new MyThread("1號線程");// t1.setName("1號線程");t1.start();System.out.println(t1.getName()); // Thread-0Thread t2 = new MyThread("2號線程");// t2.setName("2號線程");t2.start();System.out.println(t2.getName()); // Thread-1// 主線程對象的名字// 哪個線程執(zhí)行它,它就會得到哪個線程對象。Thread m = Thread.currentThread();m.setName("最牛的線程");System.out.println(m.getName()); // mainfor (int i = 1; i <= 5; i++) {System.out.println(m.getName() + "線程輸出:" + i);}}
}
掌握sleep方法,join方法的作用
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** sleep方法,join方法的作用。*/
public class ThreadTest2 {public static void main(String[] args) throws Exception {System.out.println(Runtime.getRuntime().availableProcessors());for (int i = 1; i <= 5; i++) {System.out.println(i);// 休眠5sif(i == 3){// 會讓當前執(zhí)行的線程暫停5秒,再繼續(xù)執(zhí)行// 項目經(jīng)理讓我加上這行代碼,如果用戶交錢了,我就注釋掉!Thread.sleep(5000);}}// join方法作用:讓當前調用這個方法的線程先執(zhí)行完。Thread t1 = new MyThread("1號線程");t1.start();t1.join();Thread t2 = new MyThread("2號線程");t2.start();t2.join();Thread t3 = new MyThread("3號線程");t3.start();t3.join();}
}
線程安全問題
線程安全問題概述
什么是線程安全問題?
多個線程,同時操作同一個共享資源的時候,可能會出現(xiàn)業(yè)務安全問題。
線程安全問題出現(xiàn)的原因?
- 存在多個線程在同時執(zhí)行
- 同時訪問一個共享資源
- 存在修改該共享資源
線程安全問題案例
取錢案例描述
需求:
小明和小紅是一對夫妻,他們有一個共同的賬戶,余額是10萬元,模擬2人同時去取錢10萬。
分析:
1. 需要提供一個賬戶類,接著創(chuàng)建一個賬戶對象代表2個人的共享賬戶。
2. 需要定義一個線程類(用于創(chuàng)建兩個線程,分別代表小明和小紅),
3. 創(chuàng)建2個線程,傳入同一個賬戶對象給2個線程處理。
4. 啟動2個線程,同時去同一個賬戶對象中取錢10萬。
出現(xiàn)線程安全問題的步驟:
模擬代碼如下:
先定義一個共享的賬戶類
public class Account {private String cardId; // 卡號private double money; // 余額。public Account() {}public Account(String cardId, double money) {this.cardId = cardId;this.money = money;}// 小明 小紅同時過來的public void drawMoney(double money) {// 先搞清楚是誰來取錢?String name = Thread.currentThread().getName();// 1、判斷余額是否足夠if(this.money >= money){System.out.println(name + "來取錢" + money + "成功!");this.money -= money;System.out.println(name + "來取錢后,余額剩余:" + this.money);}else {System.out.println(name + "來取錢:余額不足~");}}public String getCardId() {return cardId;}public void setCardId(String cardId) {this.cardId = cardId;}public double getMoney() {return money;}public void setMoney(double money) {this.money = money;}
}
在定義一個取錢的線程類
public class DrawThread extends Thread{private Account acc;public DrawThread(Account acc, String name){super(name);this.acc = acc;}@Overridepublic void run() {// 取錢(小明,小紅)acc.drawMoney(100000);}
}
最后,再寫一個測試類,在測試類中創(chuàng)建兩個線程對象
public class ThreadTest {public static void main(String[] args) {// 1、創(chuàng)建一個賬戶對象,代表兩個人的共享賬戶。Account acc = new Account("ICBC-110", 100000);// 2、創(chuàng)建兩個線程,分別代表小明 小紅,再去同一個賬戶對象中取錢10萬。new DrawThread(acc, "小明").start(); // 小明new DrawThread(acc, "小紅").start(); // 小紅}
}
執(zhí)行結果
某個執(zhí)行結果:
小明來取錢100000.0成功!
小紅來取錢100000.0成功!
小紅來取錢后,余額剩余:-100000.0
小明來取錢后,余額剩余:0.0
線程同步
概述
線程同步解決線程安全問題的方案。
線程同步的思想
讓多個線程實現(xiàn)先后依次訪問共享資源,這樣就解決了安全問題。
線程同步的常見方案
加鎖:每次只允許一個線程加鎖,加鎖后才能進入訪問,訪問完畢后自動解鎖,然后其他線程才能再加鎖進來。
Java提供了三種方案:
- 同步代碼塊
- 同步方法
- Lock鎖
1. 同步代碼塊
作用:把訪問共享資源的核心代碼給上鎖,以此保證線程安全。
synchronized(同步鎖){訪問共享資源的核心代碼
}
原理:每次只允許一個線程加鎖后進入,執(zhí)行完畢后自動解鎖,其他線程才可以進來執(zhí)行。
同步鎖的注意事項
對于當前同時執(zhí)行的線程來說,同步鎖必須是同一把(同一個對象),否則會出bug。
代碼示例
在共享賬戶類里使用同步代碼塊,來解決前面代碼里面的線程安全問題。我們只需要修改Account類中的代碼即可。
// 小明 小紅線程同時過來的
public void drawMoney(double money) {// 先搞清楚是誰來取錢?String name = Thread.currentThread().getName();// 1、判斷余額是否足夠// this表示該賬戶對象,正好代表共享資源!synchronized (this) {if(this.money >= money){System.out.println(name + "來取錢" + money + "成功!");this.money -= money;System.out.println(name + "來取錢后,余額剩余:" + this.money);}else {System.out.println(name + "來取錢:余額不足~");}}
}
執(zhí)行結果:
小明來取錢100000.0成功!
小明來取錢后,余額剩余:0.0
小紅來取錢:余額不足~
鎖對象如何選擇的問題
1. 建議把共享資源作為鎖對象, 不要將隨便無關的對象當做鎖對象
我們把鎖改為"鎖" 這樣一個字符串也行 因為這個資源在內存中永遠只有一份
所以各個線程需要去競爭 但是這樣好不好? 明顯不行 萬一有另外倆個人再創(chuàng)了一個賬戶 那就變成了四個人競爭一把鎖了
2. 對于實例方法,建議使用this作為鎖對象
3. 對于靜態(tài)方法,建議把類的字節(jié)碼(類名.class)當做鎖對象這里是Account.class
2. 同步方法
同步方法,就是把整個方法給鎖住,一個線程調用這個方法,另一個線程調用的時候就執(zhí)行不了,只有等上一個線程調用結束,下一個線程調用才能繼續(xù)執(zhí)行,同樣是修改Account類中的代碼即可。
修飾符 synchronized 返回值類型 方法名稱(形參列表){操作共享資源的代碼
}
原理:每次只能一個線程進入,執(zhí)行完畢以后自動解鎖,其他線程才可以進來執(zhí)行。
同步方法底層原理
- 同步方法其實底層也是有隱式鎖對象的,只是鎖的范圍是整個方法代碼。
- 如果方法是實例方法:同步方法默認用this作為的鎖對象
- 如果方法是靜態(tài)方法:同步方法默認用類名.class作為的鎖對象。
示例代碼如下:
// 同步方法
public synchronized void drawMoney(double money) {// 先搞清楚是誰來取錢?String name = Thread.currentThread().getName();// 1、判斷余額是否足夠if(this.money >= money){System.out.println(name + "來取錢" + money + "成功!");this.money -= money;System.out.println(name + "來取錢后,余額剩余:" + this.money);}else {System.out.println(name + "來取錢:余額不足~");}
}
同步代碼塊和同步方法區(qū)別
1.不存在哪個好與不好,只是一個鎖住的范圍大,一個范圍小
其中鎖的范圍小一點 性能稍微好一點 可以提前加載那些公共區(qū)域的代碼 但是提升的性能對于現(xiàn)在的計算機來說可以忽略不計
反而同步方法的可讀性要好一些
2.同步方法是將方法中所有的代碼鎖住
3.同步代碼塊是將方法中的部分代碼鎖住
3. Lock鎖
Lock鎖是JDK5開始提供的一個新的鎖定操作,通過它可以創(chuàng)建出鎖對象進行加鎖和解鎖,更靈活、更方便、更強大。
Lock是接口,不能直接實例化,可以采用它的實現(xiàn)類ReentrantLock來構建Lock鎖對象。
Lock鎖是JDK5版本專門提供的一種鎖對象,通過這個鎖對象的方法來達到加鎖,和釋放鎖的目的,使用起來更加靈活。格式如下
1.首先在成員變量位置,需要創(chuàng)建一個Lock接口的實現(xiàn)類對象(這個對象就是鎖對象)private final Lock lk = new ReentrantLock();
2.在需要上鎖的地方加入下面的代碼lk.lock(); // 加鎖//...中間是被鎖住的代碼...lk.unlock(); // 解鎖
使用Lock鎖改寫前面DrawThread中取錢的方法,代碼如下
// 創(chuàng)建了一個鎖對象
//因為倆個線程公用一個賬戶 所以建立一個實例變量作為鎖是可以的
//用final修飾更專業(yè) 防止二次賦值
private final Lock lk = new ReentrantLock();public void drawMoney(double money) {// 先搞清楚是誰來取錢?String name = Thread.currentThread().getName();try {//用try cath finally寫更專業(yè) 因為你不能保證被鎖的代碼沒有bug 有bug也要及時解鎖lk.lock(); // 加鎖// 1、判斷余額是否足夠if(this.money >= money){System.out.println(name + "來取錢" + money + "成功!");this.money -= money;System.out.println(name + "來取錢后,余額剩余:" + this.money);}else {System.out.println(name + "來取錢:余額不足~");}} catch (Exception e) {e.printStackTrace();} finally {lk.unlock(); // 解鎖}}
}
運行程序結果,觀察線程安全問題已解決。
注意事項:
-
lock鎖需要使用final修飾更專業(yè) 防止二次賦值
private final Lock lk = new ReentrantLock(); -
加鎖和解鎖時用try cath finally寫更專業(yè) 因為你不能保證被鎖的代碼沒有bug 有bug也要及時解鎖
線程通信
概述
什么是線程通信?
當多個線程共同操作共享的資源時,線程間通過某種方式互相告知自己的狀態(tài),以相互協(xié)調,并避免無效的資源爭奪。
線程通信的常見模型(生產(chǎn)者與消費者模型)
- 生產(chǎn)者線程負責生產(chǎn)數(shù)據(jù)
- 消費者線程負責消費生產(chǎn)者生產(chǎn)的數(shù)據(jù)。
- 注意:生產(chǎn)者生產(chǎn)完數(shù)據(jù)應該等待自己,通知消費者消費;消費者消費完數(shù)據(jù)也應該等待自己,再通知生產(chǎn)者生產(chǎn)!
線程通信案例
比如下面案例中,有3個廚師(生產(chǎn)者線程),兩個顧客(消費者線程)。
案例的思路:
1.先確定在這個案例中,什么是共享數(shù)據(jù)?答:這里案例中桌子是共享數(shù)據(jù),因為廚師和顧客都需要對桌子上的包子進行操作。2.再確定有那幾條線程?哪個是生產(chǎn)者,哪個是消費者?答:廚師是生產(chǎn)者線程,3條生產(chǎn)者線程; 顧客是消費者線程,2條消費者線程3.什么時候將哪一個線程設置為什么狀態(tài)生產(chǎn)者線程(廚師)放包子:1)先判斷是否有包子2)沒有包子時,廚師開始做包子, 做完之后把別人喚醒,然后讓自己等待3)有包子時,不做包子了,直接喚醒別人、然后讓自己等待消費者線程(顧客)吃包子:1)先判斷是否有包子2)有包子時,顧客開始吃包子, 吃完之后把別人喚醒,然后讓自己等待3)沒有包子時,不吃包子了,直接喚醒別人、然后讓自己等待
注意:上述方法應該使用當前同步鎖對象進行調用。
釋放當前鎖對象時,必須先喚醒其他線程,再釋放自己所占鎖
案例代碼實現(xiàn)
按照上面分析的思路和java Object提供的api寫代碼。先寫桌子類,代碼如下
public class Desk {private List<String> list = new ArrayList<>();// 放1個包子的方法// 廚師1 廚師2 廚師3//實例方法默認用this作為鎖 所以可以保證鎖住5個線程 它們公用一個桌子對象//鎖也是可以跨方法的public synchronized void put() {try {String name = Thread.currentThread().getName();// 判斷是否有包子。if(list.size() == 0){list.add(name + "做的肉包子");System.out.println(name + "做了一個肉包子~~");Thread.sleep(2000);//讓程序跑慢點容易觀察// 喚醒別人, 等待自己this.notifyAll();//必須用當前同步鎖對象進行調用 否則會出bugthis.wait();//因為只有鎖對象知道當前誰占據(jù)著它 誰需要等待}else {// 有包子了,不做了。// 喚醒別人, 等待自己this.notifyAll();//注意!!!notifyAll()和wait()位置不能調換this.wait();//你如果先wait了 你讓自己等待了 那你還怎么喚醒別人}} catch (Exception e) {//攔截sleep異常e.printStackTrace();}}// 吃貨1 吃貨2public synchronized void get() {try {String name = Thread.currentThread().getName();if(list.size() == 1){// 有包子,吃了System.out.println(name + "吃了:" + list.get(0));list.clear();Thread.sleep(1000);this.notifyAll();this.wait();}else {// 沒有包子this.notifyAll();this.wait();}} catch (Exception e) {e.printStackTrace();}}
}
再寫測試類,在測試類中,創(chuàng)建3個廚師線程對象,再創(chuàng)建2個顧客對象,并啟動所有線程
public class ThreadTest {public static void main(String[] args) {// 需求:3個生產(chǎn)者線程,負責生產(chǎn)包子,每個線程每次只能生產(chǎn)1個包子放在桌子上// 2個消費者線程負責吃包子,每人每次只能從桌子上拿1個包子吃。Desk desk = new Desk();// 創(chuàng)建3個生產(chǎn)者線程(3個廚師)new Thread(() -> {//匿名內部類寫法while (true) {desk.put();}}, "廚師1").start();new Thread(() -> {while (true) {desk.put();}}, "廚師2").start();new Thread(() -> {while (true) {desk.put();}}, "廚師3").start();// 創(chuàng)建2個消費者線程(2個吃貨)new Thread(() -> {while (true) {desk.get();}}, "吃貨1").start();new Thread(() -> {while (true) {desk.get();}}, "吃貨2").start();}
}
執(zhí)行結果如下:
廚師1做了一個肉包子~~
吃貨2吃了:廚師1做的肉包子
廚師3做了一個肉包子~~
吃貨1吃了:廚師3做的肉包子
廚師1做了一個肉包子~~
吃貨1吃了:廚師1做的肉包子
廚師3做了一個肉包子~~
吃貨1吃了:廚師3做的肉包子
廚師1做了一個肉包子~~
吃貨2吃了:廚師1做的肉包子
//不終止則一直運行下去 可以發(fā)現(xiàn)沒有出現(xiàn)線程安全問題
線程池
線程池概述
-
什么是線程池?
線程池就是一個可以復用線程的技術。 -
不使用線程池的問題:
用戶每發(fā)起一個請求,后臺就需要創(chuàng)建一個新線程來處理,下次新任務來了肯定又要創(chuàng)建新線程處理的,而創(chuàng)建新線程的開銷是很大的,并且請求過多時,肯定會產(chǎn)生大量的線程出來,這樣會嚴重影響系統(tǒng)的性能。 -
線程池解決的問題:
使用線程池,就可以解決上面的問題。線程池內部會有一個容器,存儲幾個核心線程,假設有3個核心線程,這3個核心線程可以處理3個任務。
但是任務總有被執(zhí)行完的時候,假設第1個線程的任務執(zhí)行完了,那么第1個線程就空閑下來了,有新的任務時,空閑下來的第1個線程可以去執(zhí)行其他任務。依此內推,這3個線程可以不斷的復用,也可以執(zhí)行很多個任務。
所以,線程池就是一個線程復用技術,它可以提高線程的利用率。
線程池創(chuàng)建
在JDK5版本中提供了代表線程池的接口ExecutorService,而這個接口下有一個實現(xiàn)類叫ThreadPoolExecutor類,使用ThreadPoolExecutor類就可以用來創(chuàng)建線程池對象。下面是它的構造器,參數(shù)比較多
用這7個參數(shù)的構造器來創(chuàng)建線程池的對象。代碼如下
ExecutorService pool = new ThreadPoolExecutor(3, //核心線程數(shù)有3個5, //最大線程數(shù)有5個。 臨時線程數(shù)=最大線程數(shù)-核心線程數(shù)=5-3=28, //臨時線程存活的時間8秒。 意思是臨時線程8秒沒有任務執(zhí)行,就會被銷毀掉。TimeUnit.SECONDS,//時間單位(秒)new ArrayBlockingQueue<>(4), //任務阻塞隊列,沒有來得及執(zhí)行的任務在任務隊列中等待Executors.defaultThreadFactory(), //用于創(chuàng)建線程的工廠對象new ThreadPoolExecutor.CallerRunsPolicy() //拒絕策略
);
關于線程池,需要注意下面的兩個問題
-
臨時線程什么時候創(chuàng)建?
注意!新任務提交時,發(fā)現(xiàn)核心線程都在忙、并且任務隊列滿了、并且還可以創(chuàng)建臨時線程,此時會創(chuàng)建臨時線程。
注意是任務隊列滿了之后才會創(chuàng)建臨時線程 而不是臨時線程滿了才加入任務隊列 -
什么時候開始拒絕新的任務?
核心線程和臨時線程都在忙、任務隊列也滿了、新任務過來時才會開始拒絕任務。
線程池執(zhí)行Runnable任務
創(chuàng)建好線程池之后,接下來我們就可以使用線程池執(zhí)行任務了。
線程池執(zhí)行的任務可以有兩種,一種是Runnable任務;一種是callable任務。
下面的execute方法可以用來執(zhí)行Runnable任務。
代碼案例
先準備一個線程任務類
public class MyRunnable implements Runnable{@Overridepublic void run() {// 任務是干啥的?System.out.println(Thread.currentThread().getName() + " ==> 輸出666~~");//為了模擬線程一直在執(zhí)行,這里睡久一點try {Thread.sleep(Integer.MAX_VALUE);} catch (InterruptedException e) {e.printStackTrace();}}
}
線程池處理任務類
執(zhí)行Runnable任務的代碼,注意閱讀注釋,對照著前面的7個參數(shù)理解。
public class ThreadPoolTest1 {public static void main(String[] args) {// 1、通過ThreadPoolExecutor創(chuàng)建一個線程池對象。ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());Runnable target = new MyRunnable();pool.execute(target); // 線程池會自動創(chuàng)建一個新線程,自動處理這個任務,自動執(zhí)行的!pool.execute(target); // 線程池會自動創(chuàng)建一個新線程,自動處理這個任務,自動執(zhí)行的!pool.execute(target); // 線程池會自動創(chuàng)建一個新線程,自動處理這個任務,自動執(zhí)行的!pool.execute(target);pool.execute(target);pool.execute(target);pool.execute(target);// 到了臨時線程的創(chuàng)建時機了pool.execute(target);pool.execute(target);// 到了新任務的拒絕時機了!pool.execute(target);// pool.shutdown(); // 等著線程池的任務全部執(zhí)行完畢后,再關閉線程池// pool.shutdownNow(); // 立即關閉線程池!不管任務是否執(zhí)行完畢!}
}
執(zhí)行結果:
pool-1-thread-5 ==> 輸出666~~
main ==> 輸出666~~
pool-1-thread-1 ==> 輸出666~~
pool-1-thread-3 ==> 輸出666~~
pool-1-thread-4 ==> 輸出666~~
pool-1-thread-2 ==> 輸出666~~
//其中123是核心線程執(zhí)行的 45是臨時線程執(zhí)行的
//注意程序還是一直運行的 線程池不會自動關閉 設計出來就是一直服務的
//main輸出是因為使用了CallerRunsPolicy()拒絕策略 新來的要拒絕的任務由主線程main執(zhí)行了
線程池執(zhí)行Callable任務
Callable任務相對于Runnable任務來說,就是多了一個返回值。
執(zhí)行Callable任務需要用到上面ExecutorService的submit方法
代碼案例
先準備一個Callable線程任務
public class MyCallable implements Callable<String> {private int n;public MyCallable(int n) {this.n = n;}// 2、重寫call方法@Overridepublic String call() throws Exception {// 描述線程的任務,返回線程執(zhí)行返回后的結果。// 需求:求1-n的和返回。int sum = 0;for (int i = 1; i <= n; i++) {sum += i;}return Thread.currentThread().getName() + "求出了1-" + n + "的和是:" + sum;}
}
再準備一個測試類,在測試類中創(chuàng)建線程池,并執(zhí)行callable任務。
public class ThreadPoolTest2 {public static void main(String[] args) throws Exception {// 1、通過ThreadPoolExecutor創(chuàng)建一個線程池對象。ExecutorService pool = new ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());// 2、使用線程處理Callable任務。Future<String> f1 = pool.submit(new MyCallable(100));Future<String> f2 = pool.submit(new MyCallable(200));Future<String> f3 = pool.submit(new MyCallable(300));Future<String> f4 = pool.submit(new MyCallable(400));// 3、執(zhí)行完Callable任務后,需要獲取返回結果。System.out.println(f1.get());System.out.println(f2.get());System.out.println(f3.get());System.out.println(f4.get());}
}
某次執(zhí)行后,結果如下所示
pool-1-thread-1求出了1-100的和是:5050
pool-1-thread-2求出了1-200的和是:20100
pool-1-thread-3求出了1-300的和是:45150
pool-1-thread-3求出了1-400的和是:80200
核心線程數(shù)量到底應該配置多少呢?
根據(jù)經(jīng)驗法則,大致參考以下原則:
- 如果是計算密集型的任務:核心線程數(shù)量 = CPU的核數(shù) + 1
- 如果是IO密集型的任務:核心線程數(shù)量 = CPU核數(shù) * 2
CPU核數(shù)查看,這個cpu是16核。
線程池工具類(Executors)
Executors是一個線程池的工具類,提供了很多靜態(tài)方法用于返回不同特點的線程池對象。
注意:這些方法的底層,都是通過線程池的實現(xiàn)類ThreadPoolExecutor創(chuàng)建的線程池對象。
測試代碼:
public class ThreadPoolTest3 {public static void main(String[] args) throws Exception {// 1、通過Executors創(chuàng)建一個線程池對象。ExecutorService pool = Executors.newFixedThreadPool(17);// 2、使用線程處理Callable任務。Future<String> f1 = pool.submit(new MyCallable(100));Future<String> f2 = pool.submit(new MyCallable(200));Future<String> f3 = pool.submit(new MyCallable(300));Future<String> f4 = pool.submit(new MyCallable(400));System.out.println(f1.get());System.out.println(f2.get());System.out.println(f3.get());System.out.println(f4.get());}
}
Executors創(chuàng)建線程池這么好用,為什么不推薦同學們使用呢?原因在這里:看下圖,這是《阿里巴巴Java開發(fā)手冊》提供的強制規(guī)范要求,在大型并發(fā)系統(tǒng)環(huán)境中容易出bug。
并發(fā)、并行和生命周期
并發(fā)和并行
1. 什么是進程、線程?
- 正常運行的程序(軟件)就是一個獨立的進程
- 線程是屬于進程,一個進程中包含多個線程
- 進程中的線程其實并發(fā)和并行同時存在
可以打開系統(tǒng)的任務管理器看看(快捷鍵:Ctrl+Shfit+Esc),自己的電腦上目前有哪些進程。
2. 什么是并發(fā)?
進程中的線程由CPU負責調度執(zhí)行,但是CPU同時處理線程的數(shù)量是有限的,為了保證全部線程都能執(zhí)行到,CPU采用輪詢機制為系統(tǒng)的每個線程服務,由于CPU切換的速度很快,給我們的感覺這些線程在同時執(zhí)行,這就是并發(fā)。
簡單記:并發(fā)就是多條線程交替執(zhí)行
3. 什么是并行?
并行指的是,多個線程同時被CPU調度執(zhí)行。如下圖所示,多個CPU核心在執(zhí)行多條線程
4. 多線程到底是并發(fā)還是并行呢?
其實多個線程在我們的電腦上執(zhí)行,并發(fā)和并行是同時存在的。
線程的生命周期
在Thread類中有一個嵌套的枚舉類叫Thread.Status,這里面定義了線程的6中狀態(tài)。如下圖所示
NEW: 新建狀態(tài),線程還沒有啟動
RUNNABLE: 可以運行狀態(tài),線程調用了start()方法后處于這個狀態(tài)
BLOCKED: 鎖阻塞狀態(tài),沒有獲取到鎖處于這個狀態(tài)
WAITING: 無限等待狀態(tài),線程執(zhí)行時被調用了wait方法處于這個狀態(tài)
TIMED_WAITING: 計時等待狀態(tài),線程執(zhí)行時被調用了sleep(毫秒)或者wait(毫秒)方法處于這個狀態(tài)
TERMINATED: 終止狀態(tài), 線程執(zhí)行完畢或者遇到異常時,處于這個狀態(tài)。
這幾種狀態(tài)之間切換關系如下圖所示
線程的六種狀態(tài)的總結
樂觀鎖與悲觀鎖
悲觀鎖:一上來就加鎖,沒有安全感,每次只能一個線程進入訪問完畢后再解鎖。是線程安全的,但是性能較差!
樂觀鎖:一開始不上鎖,認為是沒有問題的,大家一起跑,等要出線程安全問題的時候才開始控制。是線程安全的,且性能較好。
下面舉例說明,先寫一個沒有鎖的多線程場景:
public static void main(String[] args) throws Exception {//需求:1個靜態(tài)變量,100個線程,每個線程對其加100次 最終值為10000Runnable target = new MyRunnable();for (int i = 1; i <= 100; i++) {new Thread(target).start();}}
public class MyRunnable implements Runnable{private int count;//用實例變量代替靜態(tài)變量 反正線程都是公用一個任務對象的 所以是可以的@Overridepublic void run() {//100次for (int i = 0; i < 100; i++) {System.out.println("count--------->"+(++count));}}
}
某次運行結果:
...
count--------->9989
count--------->9990
count--------->9991
count--------->9992
count--------->9993
count--------->9994
count--------->9995
count--------->9996
count--------->9997
count--------->9998
count--------->9999
//雖然概率比較小 還是出現(xiàn)了一次線程安全問題
//有一次增值計算重疊了 沒有加到10000
樂觀鎖解決:
首先解釋原理,安全問題來自于,比如當count是10時,倆個線程幾乎同時進入,將其值修改成11,于是便發(fā)生了安全問題,少加了一次。樂觀鎖采用CAS算法(可以自己進入count.incrementAndGet()源碼看看),在加之前就記錄了count的原來的值,比如當線程進入時記錄count是10,然后將其加到11準備寫入時,發(fā)現(xiàn)count已經(jīng)變成11了,于是會將這次修改寫入作廢,重復上述過程,重新加一次。
代碼如下:
public class MyRunnable implements Runnable{//整數(shù)修改的樂觀鎖:用java的原子類實現(xiàn)的private AtomicInteger count = new AtomicInteger();@Overridepublic void run() {//100次for (int i = 0; i < 100; i++) {System.out.println("count--------->"+(count.incrementAndGet()));}}
}
執(zhí)行結果可以發(fā)現(xiàn)沒有線程安全問題。