就在前幾天,有位讀者朋友私信宜春,說期待出一篇多線程的文章,我當時內心是小鹿亂撞啊....於是這幾天茶不思飯不想,好幾天深夜皆是輾轉反側,兩目深凝,以至於這幾天走起路來格外飄飄然,左搖右晃的,魔鬼般的步伐,一般兩步,走在大馬路中央上差點被打我承認太誇張了,感覺又要被打。最終還是君意不可違,答應了這位讀者朋友,從這位讀者朋友的博客頭像可以看的出來,這位朋友絕bi歷經滄桑,對生活無盡的坦然浩對,看透俗世凡塵、世態炎涼、趨炎附勢,擁有着極高的安心恬盪情懷...啥?啥子?這個是系統默認頭像....嗯嗯嗯呃。。。那個那個宜春啥都沒說哈,別把什么事都扯宜春身上,你們一天天的,我啥都沒說(理直氣壯)...
@
1. 理解線程與進程
由於並發肯定涉及到多線程,因此在進入並發編程主題之前,我們先來了解一下進程和線程的由來,這對后面對並發編程的理解將會有很大的幫助。
進程和線程的對比這一知識點由於過於基礎,正因為過於基礎,所以我們更應該透徹它!我們必須掌握什么是線程和進程,掌握線程與進程的關系、區別及優缺點 !
1.1、何為進程?
首先我們來看一下進程的概念:
進程:是指一個內存中運行的應用程序,每個進程都有一個獨立的內存空間,一個應用程序可以同時運行多個進程;進程也是程序的一次執行過程,是系統運行程序的基本單位;系統運行一個程序即是一個進程從創建、運行到消亡的過程。
看完之后,是不是感覺很抽象?很懵bi?懵bi就對了,說明你和我智商一樣高....開個玩笑
不妨先憋棄上面的概念,放松一下大腦,雙擊打開LOL,秒選德馬打野,輸了直接退出游戲並且保持微笑,然后正襟危坐心平氣和的看宜春寫的博客....
這個時候的你不僅僅是愉快的擼了一把游戲,而且還親自體驗擼了一把進程...其實在你雙擊打開LOL的時候就已經創建了進程,此話怎講?眾所周知,我們的電腦安裝的軟件比如:LOL、微信、谷歌等等都是存儲在我們的硬盤上的,硬盤上的數據可以說是永久存儲(ORM),當我們雙擊LOL的時候,LOL程序執行就進入了內存中,所有的程序必須進入內存中才能執行,內存屬於臨時存儲(RAM),而進入內存的程序都可以叫做是進程,把LOL程序退出的時候,LOL程序就會退出內存,進程也就隨之銷毀了!因此說各位擼了一把進程也不為過吧。
啥?字太多了,看的不夠明了,不如看圖得勁....額。。。
上面主要是通過抽象的描述了進程,其實進程是可以很直觀的看的到的,我們可以再電腦底部任務欄,右鍵----->打開任務管理器,可以查看當前任務的進程:
其實,關於線程博主我完全可以一兩句話概括,但是這樣並不負責,畢竟這篇文章標題就是要讓你徹底入門java多線程。如果連進程都理解不好談何徹底理解多線程?
1.2、何為線程?
同樣的,我們先來看線程的概念
線程是進程中的一個執行單位,負責當前進程中程序的執行。一個進程中至少有一個線程,也就是說一個進程可以有多個線程的,而多個線程的進程運用程序就叫做多線程程序
線程的概念稍微好理解很多,但是想更深層次的去理解光靠上面一段文字的概述是完全不夠的!
這不打LOL的過程中,屬實卡的一批,果然花高價998買的6手戴爾筆記本打LOL屬實像極了愛情。這個時候不得不雙擊打開電腦安全管家進行殺毒,果然2500天沒有進行過病毒查殺,我天。。。其實我相信很多人都用過電腦管家或者手機管家之類的安全軟件,我們都很清楚我們開啟病毒查殺之后一般要幾分鍾掃描查殺,這個時候我們是可以讓它后台進行的,我們不會等而是開啟另一個垃圾清理的功能,這個時候我們也不會等而是再去啟動電腦加速功能。等到 這些操作都完成之后果斷退出電腦管家,繼續LOL,果然高價998買的6手戴爾筆記本再怎么殺毒打LOL還是照樣的卡....
其實清楚線程必然涉及到CPU的相關概念了,將上面文字所描述的用圖片概括,大致為:
1.3、何為多線程?
從上一節中,我們也提到過多線程,所以理解起來應該不難。
多線程就是多個線程同時運行 或 交替運行。
單核CPU:交替運行。
多核CPU:同時運行。
其實,多線程程序並不能提高程序的運行速度,但能夠提高程序運行效率,讓CPU的使用率更高。
1.4、何為線程調度優先級?
說起線程調度優先級這個概念,就讓我想到現在我們大部分人投簡歷一樣。如果你的學歷或者工作經驗越高,那么你的優先級就越高,面試官很大幾率就會讓你去面試但也不是一定只是幾率特別大,如果線程的優先級相同,那么會隨機選擇一個(線程隨機性)!在我們每個人的電腦中線程是可以設置線程的優先級的,但是生活中沒有優先級(學歷、工作經驗)的孩子就只能靠自己的能力了媽耶,太真實了...
線程優先級具有繼承特性比如A線程啟動B線程,則B線程的優先級和A是一樣的。
線程優先級具有隨機性也就是說線程優先級高的不一定每一次都先執行完,只是被執行的可能性更大。
在今后的多線程學習旅游中我們會使用到getPriority()
方法獲取線程的優先級。
1.5、為什么提倡使用多線程而不是多進程?
線程與進程相似,但線程是一個比進程更小的執行單位,是程序執行的最小單位。一個進程在其執行的過程中可以產生多個線程。與進程不同的是同類的多個線程共享同一塊內存空間和一組系統資源,所以系統在產生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多,也正因為如此,線程也被稱為輕量級進程。同時線程是程序執行的最小單位。使用多線程而不是用多進程去進行並發程序的設計,是因為線程間的切換和調度的成本遠遠小於進程。
而使用多線程,多線程會將程序運行方式從串行運行變為並發運行,效率會有很大提高。
2、理解並行和並發
在博主認為並發和並行是兩個非常容易被混淆的概念。為了防止繞暈大家,所以我選擇長話短說!
- 並發:一個
時間段
內同時發生(並不是同時發生)。 - 並行:同一
時刻
發生(真正的同時發生)。
它們都可以表示兩個或者多個任務一起執行,但是偏重點有些不同。
於此同時,我們不妨回顧一下上面所提到過的CPU,並再次理解並發與並行的區別,從而溫故知新 我TM簡直是個天才!
單核CPU:交替運行【並發】
多核CPU:同時運行【並行】
並發給人的感覺是同時運行,那是因為分時交替運行的時間是非常短的!
3、特殊的一個單線程:主線程(Main線程)
我們常說的主線程就是Main線程,它是一個特殊的單線程,話不多說,直接擼碼:
定義一個用於測試的demo類Person
package demo;
public class Person {
public String name;
public Person(String name){
this.name=name;
}
public void run(){
int i=1;
while (i<5){
System.out.println(name+i);
i++;
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
編寫Main方法
package demo;
public class MainThreadDemo {
public static void main(String[] args) {
Person per=new Person("常威");
per.run();
Person Per2=new Person("來福");
Per2.run();
}
}
運行結果就已經很顯而易見了,放心我不是靠你們運行結果而是單純的先分析主線程。
運行結果:
常威1
常威2
常威3
常威4
來福1
來福2
來福3
來福4
3.1、分析主線程原理
3.2、 單線程的局限性
單線程不僅效率低下,而且存在很大的局限性,惟一的優點就是安全。所以說女孩子長得安全其實也是一種優點,噗哈哈哈...
如何體現出單線程效率低下以及它的局限性呢?其實只要一句代碼即可,還是以上面的單線程Main線程為例:
package demo;
public class MainThreadDemo {
public static void main(String[] args) {
Person per=new Person("常威");
per.run();
int a=6/0; //=====================特別注意這行代碼
Person Per2=new Person("來福");
Per2.run();
}
}
試想一下運行結果...
如果對上面的運行結果有問題,或者疑問。那沒錯了,你簡直是個天(小)才(白)!真真的天(小)才(白),很有可能異常機制沒學好,好吧我給你貼出來:【java基礎之異常】死了都要try,不淋漓盡致地catch我不痛快!
言歸正傳,效率低下何以見得?這是數據少,如果是一億條數據呢,單線程就是一個一個打印。那局限性又何以見得呢?從上面運行結果來看也能看出,只因為一行代碼而導致下面代碼不再執行。已經很明顯了。
4、 創建多線程的四種方式
說是說創建多線程有四種方式,但考慮到是入門文章還是主要寫入門的兩種方式,剩下的兩個暫時忽略。忽略的兩種方法有:實現Callable
接口通過FutureTask
包裝器來創建Thread線程、使用ExecutorService
、Callable
、Future
實現有返回結果的線程。現在可能對於入門的童鞋來說是接收不了的,以后再去了解也不晚!
4.1、繼承Thread類
Java使用java.lang.Thread
類代表線程,所有的線程對象都必須是Thread類或其子類的實例。每個線程的作用是完成一定的任務,實際上就是執行一段程序流即一段順序執行的代碼。Java使用線程執行體來代表這段程序流。
Java中通過繼承Thread
類來創建並啟動多線程的步驟如下:
- 定義
Thread
類的子類,並重寫該類的run()
方法,該run()
方法的方法體就代表了線程需要完成的任務,因此把run()
方法稱為線程執行體。 - 創建
Thread
子類的實例,即創建了線程對象 - 調用線程對象的
start()
方法來啟動該線程
代碼如下:
測試類:
public class Demo01 {
public static void main(String[] args) {
//創建自定義線程對象
MyThread mt = new MyThread("新的線程!");
//開啟新線程
mt.start();
//在主方法中執行for循環
for (int i = 0; i < 10; i++) {
System.out.println("main線程!"+i);
}
}
}
自定義線程類:
public class MyThread extends Thread {
//定義指定線程名稱的構造方法
public MyThread(String name) {
//調用父類的String參數的構造方法,指定線程的名稱
super(name);
}
/**
* 重寫run方法,完成該線程執行的邏輯
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在執行!"+i);
}
}
}
Thread
類本質上是實現了Runnable
接口的一個實例,代表一個線程的實例。啟動線程的唯一方法就是通過Thread
類的start()
實例方法。start()
方法是一個native
方法,它將啟動一個新線程,並執行run()
方法。這種方式實現多線程很簡單,通過自己的類直接extend Thread
,並復寫run()
方法,就可以啟動新線程並執行自己定義的run()
方法。
4.2、實現Runnable接口
如果自己的類已經繼承另一個類,就無法直接繼承Thread
,此時,可以實現一個Runnable
接口來創建線程,顯然實現Runnable
接口方式創建線程的優勢就很明顯了。
直接擼碼:
自定義一個類實現Runnable接口,並重寫接口中的run()方法,並為run方法添加要執行的代碼方法。
public class RunableDemo implements Runnable{
@Override
public void run() {
int a = 1;
while (a<20){
System.out.println(Thread.currentThread().getName()+ a);//Thread.currentThread().getName()為獲取當前線程的名字
a++;
}
}
}
編寫Main方法
為了啟動自定義類RunableDemo
,需要首先實例化一個Thread,並傳入RunableDemo 實例:
public class MainThreadDemo {
public static void main(String[] args) {
RunableDemo runn=new RunableDemo();
//實例化一個Thread並傳入自己的RunableDemo 實例
Thread thread=new Thread(runn);
thread.start();
int a = 1;
while (a<20){
//Thread.currentThread().getName()為獲取當前線程的名字
System.out.println(Thread.currentThread().getName()+ a);
a++;
}
}
}
運行結果:
main1
main2
main3
Thread-01
Thread-02
Thread-03
Thread-04
Thread-05
Thread-06
....
其實多運行幾遍,你會方法每次運行的結果順序都不一樣,這主要是由於多線程會去搶占CPU的資源,誰搶到了誰就執行,而Main和Thread兩個線程一直在爭搶。
實際上,當傳入一個Runnable target
(目標)參數給Thread
后,Thread
的run()
方法就會調用target.run()
,參考JDK源代碼:
public void run() {
if (target != null) {
target.run();
}
}
4.3、兩種入門級創建線程的區別
采用繼承Thread類方式:
(1)優點:編寫簡單,如果需要訪問當前線程,無需使用Thread.currentThread()方法,直接使用this,即可獲得當前線程。
(2)缺點:因為線程類已經繼承了Thread類,所以不能再繼承其他的父類。
采用實現Runnable接口方式:
(1)優點:線程類只是實現了Runable接口,還可以繼承其他的類。在這種方式下,可以多個線程共享同一個目標對象,所以非常適合多個相
同線程來處理同一份資源的情況,從而可以將CPU代碼和數據分開,形成清晰的模型,較好地體現了面向對象的思想。
(2)缺點:編程稍微復雜,如果需要訪問當前線程,必須使用Thread.currentThread()方法。
小結:
如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable
接口的話,則很容易的實現資源共享。
實現Runnable
接口比繼承Thread
類的優勢:
1.適合多個相同代碼的線程去處理同一個資源。
2.可以避免java中單繼承的限制。
3.增加代碼的健壯性,實現解耦。代碼可以被多個線程共享,代碼和數據獨立。
4.線程池中只能放入實現Runnable或Callable類線程,不能放入繼承Thread的類【線程池概念之后會慢慢涉及】
所以,如果選擇哪種方式,盡量選擇實現Runnable
接口!
其實學到后面的線程池,你會發現上面兩種創建線程的方法實際上很少使用,一般都是用線程池的方式比較多一點。使用線程池的方式也是最推薦的一種方式,另外,《阿里巴巴Java開發手冊》在第一章第六節並發處理這一部分也強調到“線程資源必須通過線程池提供,不允許在應用中自行顯示創建線程”。不過處於入門階段的童鞋博主還是強烈建議一步一個腳印比較好!
5、使用匿名內部類方式創建線程
談起匿名內部類,可能很多小白是比較陌生的,畢竟開發中使用的還是比較少,但是同樣是非常重要的一個知識!於此同時我就貼出關於匿名內部類的文章程序員你真的理解匿名內部類嗎?如果小白童鞋能看懂下面這個代碼,真的你不需要看那篇文章了,你T喵的簡直是個天才!
package AnonymousInner;
public class NiMingInnerClassThread {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
for (int i = 0; i<5;i++){
System.out.println("熊孩子:"+i);
}
}
};
new Thread(r).start();
for (int i = 0; i < 5 ; i++){
System.out.println("傻狍子:"+i);
}
}
}
小白童鞋還愣着干啥呀趕緊去補補...
6、線程安全問題
線程安全問題主要是共享資源競爭的問題,也就是在多個線程情況下,一個或多個線程同時搶占同一資源導致出現的一些不必要的問題,最典型的例子就是火車四個窗口售票問題了,這里就不再舉售票例子了,已經爛大街了,這里就簡單實現一個線程安全問題代碼....
實現Runnable接口方式為例,主要實現過程是:實例化三個Thread,並傳入同一個RunableDemo 實例作為參數,最后開啟三條相同參數的線程,代碼如下:
public class RunableDemo implements Runnable{
public int a = 100;//線程共享數據
@Override
public void run() {
while (a>0){
System.out.println("線程"+Thread.currentThread().getName()+"執行到"+ a);
a--;
}
}
}
public class MainThreadDemo {
public static void main(String[] args) {
RunableDemo runn=new RunableDemo();
Thread thread1=new Thread(runn);
Thread thread2=new Thread(runn);
Thread thread3=new Thread(runn);
thread1.start();
thread2.start();
thread3.start();
}
}
運行結果:
Thread-0==100
Thread-0==99
Thread-1==100
Thread-1==97
Thread-1==96
Thread-1==95
Thread-2==98
...
根據結果可以看出,確實是三條線程(Thread-0、1、2)在執行,安全問題就出在線程會出現相同的結果比如上面的100就出現了兩次,如果循環條件更改一下可能也會出現負數的情況。這種情況該怎么解決呢?這個時候就需要線程同步了!
7、解決線程安全問題:線程同步
實際上,線程安全問題的解決方法有三種:
1、同步代碼塊
2、同步方法
3、鎖機制
7.1、 synchronized同步代碼塊
第一種方法:同步代碼塊
格式:
synchronized(鎖對象) {
可能會出現線程安全問題的代碼(訪問共享數據的代碼)
}
使用同步代碼塊特別注意:
1、通過代碼塊的鎖對象,可以是任意對象
2、必須保證多個線程使用的鎖對象必須是同一個
3、鎖對象的作用是把同步代碼快鎖住,只允許一個線程在同步代碼塊執行
還是以上面線程安全問題為例子,使用同步代碼塊舉例:
public class RunableDemo implements Runnable{
public int a = 100;//線程共享數據
Object object=new Object(); //事先准備好一個鎖對象
@Override
public void run() {
synchronized (object){ //使用同步代碼塊
while (a>0){
System.out.println("線程"+Thread.currentThread().getName()+"執行到"+ a);
a--;
}
}
}
}
Main方法沒有任何改動,運行一下結果是絕對沒問題的,數據都是正確的沒有出現重復情況這一出,各位可以自己嘗試一下!
同步代碼塊的原理:
使用了一個鎖對象,叫同步鎖,對象鎖,也叫同步監視器,當開啟多個線程的時候,多個線程就開始搶奪CPU的執行權,比如現在t0線程首先的到執行,就會開始執行run方法,遇到同步代碼快,首先檢查是否有鎖對象,發現有,則獲取該鎖對象,執行同步代碼塊中的代碼。之后當CUP切換線程時,比如t1得到執行,也開始執行run方法,但是遇到同步代碼塊檢查是否有鎖對象時發現沒有鎖對象,t1便被阻塞,等待t0執行完畢同步代碼塊,釋放鎖對象,t1才可以獲取從而進入同步代碼塊執行。
同步中的線程,沒有執行完畢是不會釋放鎖的,這樣便實現了線程對臨界區的互斥訪問,保證了共享數據安全。
缺點:頻繁的獲取釋放鎖對象,降低程序效率
7.2、同步方法
使用步驟:
1、把訪問了共享數據的代碼抽取出來,放到一個方法中
2、在該方法上添加synchronized
修飾符
格式:
修飾符 synchronized 返回值類型 方法名稱(參數列表) {
方法體...
}
代碼示例:
public class RunableDemo implements Runnable{
public int a = 100;//線程共享數據
@Override
public void run() {
while (true){
sell(); //調用下面的sell方法
}
}
//訪問了共享數據的代碼抽取出來,放到一個方法sell中
public synchronized void sell(){
while (a>0){
System.out.println("線程"+Thread.currentThread().getName()+"執行到"+ a);
a--;
}
}
}
同步方法的也是一樣鎖住同步的代碼,但是鎖對象的是Runable
實現類對象,也就是this
,誰調用方法,就是誰。
說到同步方法,就不得不說一下靜態同步方法,顧名思義,就是在同步方法上加上static,靜態的同步方法,添加一個靜態static
修飾符,此時鎖對象就不是this
了,靜態同步方法的鎖對象是本類的class屬性,class文件對象(反射)
public class RunableDemo implements Runnable{
public static int a = 100;//線程共享數據 =====此時共享數據也要加上static
@Override
public void run() {
while (true){
sell();
}
}
public static synchronized void sell(){ //注意添加了static關鍵字
while (a>0){
System.out.println("線程"+Thread.currentThread().getName()+"執行到"+ a);
a--;
}
}
}
使用靜態同步方法時,此時共享數據也要加上static,因為static成員才能訪問static成員,如果對static關鍵字不是他別理解的可以補補了,放心,博主有信心讓你有所收獲,會讓你重新認識到static的魅力:深入理解static關鍵字
當然靜態同步方法了解即可!
7.3、Lock鎖
Lock接口位於java.util.concurrent.locks.Lock
它是JDK1.5之后出現的,Lock接口中的方法:
void lock()
: 獲取鎖
void unlock()
: 釋放鎖
Lock接口的一個實現類java.util.concurrent.locks.ReentrantLock implements Lock
接口
使用方法:
1、在Runable
實現類的成員變量創建一個ReentrantLock
對象
2、在可能產生線程安全問題的代碼前該對象調用lock
方法獲取鎖
3、在可能產生線程安全問題的代碼后該對象調用unlock
方法釋放鎖
代碼示例:
import java.util.concurrent.locks.ReentrantLock;
public class RunableDemo implements Runnable{
public static int a = 100;//線程共享數據
//1、在Runable實現類的成員變量創建一個ReentrantLock對象============
ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
// 2、在可能產生線程安全問題的代碼前該對象調用lock方法獲取鎖=======
reentrantLock.lock();
while (a>0){
System.out.println("線程"+Thread.currentThread().getName()+"執行到"+ a);
a--;
}
// 3、在可能產生線程安全問題的代碼后該對象調用unlock方法獲取鎖======
reentrantLock.unlock();
}
}
當然更安全的寫法是,在線程安全問題代碼中try...catchy
,最后在finally
語句中添加reentrantLock.unlock();
,這樣方為上上策!
7.4、三種方法小結
第一種
synchronized 同步代碼塊:可以是任意的對象必須保證多個線程使用的鎖對象是同一個
第二種
synchronized 同步方法: 鎖對象是this,誰調用鎖對象就是誰
synchronized 靜態同步方法: 鎖對象是其class對象,該對象可以用
this.getClass()
方法獲取,也可以使用當前類名.class
表示。【了解即可】
第三種
Look鎖方法:該方法提供的方法遠遠多於synchronized
方式,主要在Runable實現類的成員變量創建一個ReentrantLock
對象,並使用該對象調用lock
方法獲取鎖以及unlock
方法釋放鎖!
8、線程常用方法
8.1、Thread類
Thread()
:用於構造一個新的Thread。
Thread(Runnable target)
:用於構造一個新的Thread,該線程使用了指定target的run方法。
Thread(ThreadGroup group,Runnable target)
:用於在指定的線程組中構造一個新的Thread,該
線程使用了指定target的run方法。
currentThread()
:獲得當前運行線程的對象引用。
interrupt()
:將當前線程置為中斷狀態。
sleep(long millis)
:使當前運行的線程進入睡眠狀態,睡眠時間至少為指定毫秒數。
join()
:等待這個線程結束,即在一個線程中調用other.join(),將等待other線程結束后才繼續本線程。
yield()
:當前執行的線程讓出CPU的使用權,從運行狀態進入就緒狀態,讓其他就緒線程執行。
8.2、Object類
wait()
:讓當前線程進入等待阻塞狀態,直到其他線程調用了此對象的notify()或notifyAll()方法后,當前線程才被喚醒進入就緒狀態。
notify()
:喚醒在此對象監控器(鎖對象)上等待的單個線程。
notifyAll()
:喚醒在此對象監控器(鎖對象)上等待的所有線程。
注意:
wait()、notify()、notifyAll()
都依賴於同步鎖,而同步鎖是對象持有的,且每個對象只有一個,所以這些方法定義在Object
類中,而不是Thread
類中。
8.3、yield()、sleep()、wait()比較
wait()
:讓線程從運行狀態進入等待阻塞狀態,並且會釋放它所持有的同步鎖。
yield()
:讓線程從運行狀態進入就緒狀態,不會釋放它鎖持有的同步鎖。
sleep()
:讓線程從運行狀態進入阻塞狀態,不會釋放它鎖持有的同步鎖。
9、線程的狀態
以上只是簡單的一個線程狀態圖,其實線程狀態博大精深,要講清楚還是要一大篇文筆,作為入門文章先了解一下吧,之后的並發編程文章將再講述吧!
如果想要去深入了解一下的話也是可以的:Java線程的6種狀態及切換
10、線程池
在java中只要說到池,基本都是一個套路,啥數據庫連接池、jdbc連接池等,思想基本上就是:一個容納多個要使用資源的容器,其中的資源可以反復使用,省去了頻繁創建線程對象的操作,無需反復創建資源而消耗過多資源。
10.1、線程池概述
線程池其實就是一個容納多個線程的容器,其中的線程可以反復使用,省去了頻繁創建線程對象的操作,無需反復創建線程而消耗過多資源。
合理利用線程池能夠帶來三個好處:
- 降低資源消耗。減少了創建和銷毀線程的次數,每個工作線程都可以被重復利用,可執行多個任務。
- 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
- 提高線程的可管理性。可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因為消耗過多的內存,而把服務器累趴下(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,最后死機)。
10.2、 線程池的使用
Java里面線程池的最頂級接口是java.util.concurrent.Executor
,但是嚴格意義上講Executor
並不是一個線程池,而只是一個執行線程的工具。真正的線程池接口是java.util.concurrent.ExecutorService
。
要配置一個線程池是比較復雜的,尤其是對於線程池的原理不是很清楚的情況下,很有可能配置的線程池不是較優的,因此在java.util.concurrent.Executors
線程工廠類里面提供了一些靜態工廠,生成一些常用的線程池。官方建議使用Executors
工程類來創建線程池對象。
Executors類中有個創建線程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
:返回線程池對象。(創建的是有界線程池,也就是池中的線程個數可以指定最大數量)
獲取到了一個線程池ExecutorService 對象,那么怎么使用呢,在這里定義了一個使用線程池對象的方法如下:
public Future<?> submit(Runnable task)
:獲取線程池中的某一個線程對象,並執行
Future接口:用來記錄線程任務執行完畢后產生的結果。線程池創建與使用。
使用線程池中線程對象的步驟:
- 創建線程池對象。
- 創建Runnable接口子類對象。(task)
- 提交Runnable接口子類對象。(take task)
- 關閉線程池(一般不操作這一步)。
Runnable實現類代碼:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一個游泳教練");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教練來了: " + Thread.currentThread().getName());
System.out.println("教我游泳,教會后,教練又回到了游泳池");
}
}
線程池測試類:
public class ThreadPoolDemo {
public static void main(String[] args) {
// 創建線程池對象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象
// 創建Runnable實例對象
MyRunnable r = new MyRunnable();
//自己創建線程對象的方式
// Thread t = new Thread(r);
// t.start(); ---> 調用MyRunnable中的run()
// 從線程池中獲取線程對象,然后調用MyRunnable中的run()
service.submit(r);
// 再獲取個線程對象,調用MyRunnable中的run()
service.submit(r);
service.submit(r);
// 注意:submit方法調用結束后,程序並不終止,是因為線程池控制了線程的關閉。
// 將使用完的線程又歸還到了線程池中
// 關閉線程池
//service.shutdown();
}
}
以上只是簡單的使用線程池,僅僅是入門階段!道阻且長,路還很長....
到這里,本文章入門暫時告一段落,以后有時間盡量抽空更新....
如果本文章對你有幫助,哪怕是一點點,請點個贊唄,謝謝你~
歡迎各位關注我的公眾號,一起探討技術,向往技術,追求技術...說好了來了就是盆友喔...