先從java開啟一個(gè)線程開始說饮怯。
首先常用的有四種方式:繼承+兩種實(shí)現(xiàn)+線程池獲取霞势。
其實(shí)我們之前大量的demo都是new Thread(()->{}).start();這個(gè)就是繼承的方式搂誉。
這里重點(diǎn)說下兩種實(shí)現(xiàn):
這里注意兩種方式的區(qū)別:
- Runnable沒有返回值莲蜘,而Callable是有返回值的(返回值是傳入的泛型類型)
- Runnable的run是不會拋異常的洗出,而Callable中的call是會拋異常的
- 兩者的需要實(shí)現(xiàn)的方法不一樣士复。
Callable的實(shí)現(xiàn)方式:
正常我們用匿名內(nèi)部類的方式:new Thread(()->{},"name")或者不需要這個(gè)name參數(shù)。但是這里其實(shí)()代表的是Runnable類型的參數(shù)翩活。所以說Callable要如何去寫呢阱洪?
那我們要怎么把Callable和Runnable掛上關(guān)系呢?如下圖結(jié)構(gòu):
所以如下圖代碼的實(shí)現(xiàn):
public static void main(String[] args) throws Exception {
FutureTask<Integer> futureTask = new FutureTask<Integer>(()->{
TimeUnit.SECONDS.sleep(2);
return 1024;
});
new Thread(futureTask).start();
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
System.out.println(futureTask.get());
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
注意雖然我直接返回了1024.但是是因?yàn)槭菧y試代碼菠镇。正常來講這個(gè)方法里的要處理業(yè)務(wù)邏輯的冗荸。可能執(zhí)行N久利耍。所以我這里睡了2秒來表示代碼執(zhí)行2s最終算出這個(gè)1024的結(jié)果蚌本。而futureTask.get()就是獲取call方法的結(jié)果盔粹。注意這個(gè)方法是可以設(shè)置等待時(shí)長的。比如方法沒執(zhí)行完會一直阻塞在那里等結(jié)果程癌。但是可以設(shè)置等xxx時(shí)間還沒結(jié)果就不等了舷嗡。
然后注意我這兩個(gè)時(shí)間輸出語句:意料之中的第一個(gè)直接打印了,而第二個(gè)等了2s獲取到這個(gè)返回值打印的嵌莉。如下運(yùn)行截圖:
反正這個(gè)get方法設(shè)置時(shí)間的和不設(shè)置時(shí)間的都試過了进萄。總而言之用法上很常規(guī)锐峭。類似阻塞隊(duì)列中鼠。然后使用的時(shí)候有一些建議:
get方法放在最后。因?yàn)間et是要等計(jì)算完成才能獲取到沿癞,所以放在最后合計(jì)的時(shí)候獲取可以節(jié)省時(shí)間援雇。比如 同時(shí)四個(gè)線程在main線程中執(zhí)行。
A-2s椎扬。B-2s惫搏。C-2s。D-2s盗舰。
如果我們在啟動A線程緊接著去get晶府,那么get本身還要等2s,然后get以后再去啟動B钻趋。再緊接著去get川陆。又等2s。最終這四個(gè)線程的結(jié)果合計(jì)要8s才能獲取到蛮位。
但是如果我們先啟動A,B,C,D.然后最后去getA,,B,C,D.這樣只需要2s我們就能獲取到這四個(gè)線程的合適了较沪。
當(dāng)然了,java中還有一點(diǎn):如果同一個(gè)task失仁,那么計(jì)算結(jié)果是可以共通的尸曼。如下代碼:
如果想每個(gè)線程都計(jì)算一次萄焦,要寫多個(gè)實(shí)例控轿。
線程池
Java中線程池是通過Executor框架實(shí)現(xiàn)的。該框架中用到了Executor拂封,Executors茬射,ExecutorService,ThreadPoolExecutor等幾個(gè)類冒签。
注意上文中有個(gè)Executors圖中沒有在抛。但是這個(gè)類是一個(gè)工具類妓柜。我們可以回憶一下
- 數(shù)組:Array授翻,然后有個(gè)數(shù)組工具類Arrays鸠蚪。
- 集合:Collection测僵,然后有個(gè)Collections工具類。
- 線程:Executor朴读,然后有個(gè)Executors工具類屹徘。
你看這么一順是不是發(fā)現(xiàn)合情又合理還好記?那這個(gè)就過了磨德,說下一個(gè)知識點(diǎn):
java中線程池有多種缘回。其分別有不同的使用場景和作用。然后每次申請的時(shí)候注意不要new5涮簟!new是新建一個(gè)啦吧,我們不要用這種方式您觉,而是應(yīng)該用工具類申請一個(gè)。而池化技術(shù)一定一定要注意的就是關(guān)閉線程授滓。甚至有時(shí)候關(guān)閉比啟用還要重要琳水!
當(dāng)然了一些用的比較少的類行就不講了。這里重點(diǎn)就講幾種:
-
初始化的時(shí)候就固定線程數(shù)的線程池:適合執(zhí)行長期的任務(wù)般堆,性能好很多
這個(gè)就是在創(chuàng)建線程池的時(shí)候就設(shè)定好線程池的容量在孝。創(chuàng)建方法如下:
ExecutorService threadPool = Executors.newFixedThreadPool(5);//初始化的時(shí)候就固定大小的線程池
這個(gè)參數(shù)的大小就是初始化線程數(shù)。然后我們可以測試使用一下(獲取線程是submit淮摔,關(guān)閉線程是shutdown):
首先線程的個(gè)數(shù)最多只有5私沮,所以說五個(gè)線程是沒問題的。其次設(shè)定是10個(gè)客戶辦理業(yè)務(wù)和橙,一共有五個(gè)窗口仔燕。一個(gè)常規(guī)思維就是一個(gè)窗口辦理兩個(gè)。但是實(shí)際上并不是魔招。有的窗口辦理了三個(gè)業(yè)務(wù)晰搀,有的只辦理了一個(gè)。實(shí)際上分析可能是有個(gè)業(yè)務(wù) 辦理時(shí)間長办斑。所以辦理的少外恕。代碼的角度來說:當(dāng)線程池歸還以后,下次從線程池獲取線程的概率是一樣的乡翅。不會因?yàn)檫@個(gè)剛用完所以讓它歇歇鳞疲。
剛剛說了初始化的時(shí)候就固定了5個(gè)線程,所以哪怕再多任務(wù)過來了峦朗,也只能有五個(gè)線程工作建丧,我把for循環(huán)設(shè)置為100,1000波势,10000也都超出不了五個(gè)線程翎朱。
- 初始化的時(shí)候只有一個(gè)線程的線程池橄维。適合一個(gè)任務(wù)一個(gè)任務(wù)執(zhí)行的場景
//只有一個(gè)線程的線程池
ExecutorService threadPool = Executors.newSingleThreadExecutor();
這個(gè)其實(shí)名字就比較容易理解,單例模式嘛拴曲,就是單個(gè)線程争舞。然后測試可以和剛剛的一樣的demo,我們簡單跑一下:
- 可伸縮擴(kuò)容的線程池。適用于執(zhí)行很多短期異步的小程序或者負(fù)載較輕的服務(wù)器澈灼。
//只有一個(gè)線程的線程池
ExecutorService threadPool = Executors.newCachedThreadPool();
這個(gè)咋說呢竞川,就是幾個(gè)夠用要幾個(gè)線程。不限制大小叁熔。依然是上面的demo:
由此說,這個(gè)線程池是根據(jù)實(shí)際情況伸縮荣回。當(dāng)然了這個(gè)底層的調(diào)度就比較復(fù)雜了遭贸。我也不清楚。心软。
注意了上面三種創(chuàng)建方式壕吹,其底層源碼都是用的一個(gè)方法:
而這個(gè)方法就是new ThreadPoolExecutor()。其中有五個(gè)參數(shù)删铃。而這五個(gè)參數(shù)對應(yīng)的意義很重要耳贬,要背下來!當(dāng)然了我們看上去只有五個(gè)參數(shù)猎唁。但是其實(shí)底層的話是一共有七個(gè)參數(shù)的咒劲。下面我們一個(gè)個(gè)介紹。
線程池七大參數(shù)
我們之前看源碼明明是五個(gè)參數(shù)胖秒,為什么這里說線程池的七大參數(shù)呢缎患?直接看源碼:
其實(shí)我們看是五個(gè)參數(shù)是因?yàn)闆]有看到底。繼續(xù)往下走就會發(fā)現(xiàn)底層都是七個(gè)參數(shù)阎肝。下面我們一個(gè)個(gè)說這七個(gè)參數(shù)都是什么:
- corePoolSize:線程池中的長駐核心線程數(shù)挤渔。
- maximumPoolSize:線程池能夠容納同時(shí)執(zhí)行的最大線程數(shù)。此值必須大于1.
- keepAliveTime:多余的空閑線程的存活時(shí)間(當(dāng)線程池中線程個(gè)數(shù)大于核心長駐數(shù)而小于最大線程數(shù)的時(shí)候會啟效果风题。也就是線程空閑到設(shè)置的時(shí)間就會自動銷毀判导,直到線程數(shù)等于corePoolSize)
- unit:keepAliveTime的單位
- workQueue:任務(wù)隊(duì)列,被提交但尚未被執(zhí)行的任務(wù)沛硅。
- threadFactory:生成線程池中工作線程的線程工廠眼刃,用于創(chuàng)建線程,一般用默認(rèn)的即可摇肌。
- handler:拒絕策略擂红,當(dāng)隊(duì)列滿了并且工作線程大于等于線程池的最大線程數(shù)(maximumPoolSize)的時(shí)候如何來拒絕新的任務(wù)。
其實(shí)上面的就是七個(gè)參數(shù)的理論了围小。下面我們結(jié)合實(shí)際來講講:
比如線程池是一個(gè)銀行昵骤。那么第一個(gè)corePoolSize:核心線程數(shù)树碱。我們可以理解為銀行總是有人值班的窗口。
而當(dāng)核心線程數(shù)滿了变秦,都在工作以后成榜,就會進(jìn)入到隊(duì)列中排隊(duì)。也就是workQueue蹦玫。
而maximumPoolSize是能容納的最大線程數(shù)赎婚。比如說銀行的常用窗口只有兩個(gè),然后都干著活樱溉,并且大廳排隊(duì)的人也滿了挣输,這時(shí)候會把其余沒有人值班的窗口也啟用。但是前提是有窗口了才能安排人值班饺窿∑缃梗總不能看人多了現(xiàn)去扒個(gè)窗口出來吧。所以我們可以理解為銀行現(xiàn)存的窗口(不管有沒有人值班)的個(gè)數(shù)就是能容納的最大個(gè)數(shù)肚医。
而這里keepAliveTime和unit是一對。是因?yàn)槿硕嗨耘R時(shí)開啟的窗口向瓷,但是假如所有人都辦完了肠套,現(xiàn)在沒有客人了,這些本來不是長期猖任,因?yàn)槿硕嗨耘R時(shí)開啟的窗口總不能也一直在這值班了你稚。所以會有個(gè)機(jī)制:比如說半小時(shí)內(nèi)還沒人來,那么這個(gè)窗口就關(guān)了朱躺。里面的工作人員該干嘛干嘛去了刁赖。
ThreadFactory就是生成線程的工廠。一般都是默認(rèn)的长搀。換成銀行的話我們可以理解為銀行中的工作人員的來源宇弛。你去銀行辦業(yè)務(wù)但是你不用管給你辦業(yè)務(wù)的服務(wù)人員是怎么來的,是分配來的還是轉(zhuǎn)行來的還是潛規(guī)則進(jìn)來的源请,這個(gè)和你都沒啥關(guān)系枪芒。
handler是拒絕策略。很容易理解:一個(gè)小營業(yè)廳在人多了會排隊(duì)谁尸,實(shí)在不行加值班窗口舅踪。但是不管怎么加還是不斷有人來,擠都擠不進(jìn)去了良蛮,這個(gè)時(shí)候只能拒絕不讓客戶進(jìn)了抽碌。而這個(gè)具體的策略是有四種實(shí)現(xiàn)的。具體要怎么拒絕是可以酌情設(shè)置的决瞳。
下面是線程池底層原理的語言敘述:
在創(chuàng)建了線程池后货徙,等待提交過來的任務(wù)請求左权。
-
當(dāng)調(diào)用execute()方法(submit底層也是調(diào)用execute方法)添加一個(gè)請求任務(wù)時(shí),線程池會做如下判斷:
- 如果正在運(yùn)行的線程數(shù)量小于corePoolSize破婆,那么馬上就創(chuàng)建線程運(yùn)行這個(gè)任務(wù)涮总。
- 如果正在運(yùn)行的線程數(shù)量大于等于corePoolSize,那么將回把這個(gè)任務(wù)放入隊(duì)列
- 如果這個(gè)時(shí)候隊(duì)列也滿了且正在運(yùn)行的線程數(shù)還小于maximumPoolSize祷舀,那么還是要?jiǎng)?chuàng)建非核心線程立刻運(yùn)行第一個(gè)等待的任務(wù)瀑梗。并且可以把這個(gè)插入到阻塞隊(duì)列了(注意阻塞隊(duì)列是不能插隊(duì)的!)
- 如果隊(duì)列滿了裳扯,并且正在運(yùn)行的線程數(shù)量大于等于maximumPoolSize抛丽,那么線程池會啟動飽和拒絕策略來執(zhí)行。
當(dāng)一個(gè)線程完成任務(wù)后饰豺,它會從隊(duì)列中獲取下一個(gè)任務(wù)來執(zhí)行亿鲜。
-
當(dāng)一個(gè)下次你哼無事可做超過一定的時(shí)間(keepAliveTime)時(shí),線程池會判斷:
- 如果當(dāng)前與性的線程池?cái)?shù)量大于corePoolSize冤吨,那么這個(gè)線程會被銷毀蒿柳。
- 線程池的所有任務(wù)執(zhí)行完成后,最終會收縮到corePoolSize的大小漩蟆。
線程池的拒絕策略
其實(shí)這個(gè)上面已經(jīng)說到過了垒探,但是這里再用文字表述下:
等待隊(duì)列已經(jīng)排滿了,再也塞不下新的任務(wù)了怠李,同事線程池中的max線程也達(dá)到了圾叼,無法繼續(xù)為新任務(wù)服務(wù)。這時(shí)候我們就需要用拒絕策略機(jī)制合理的處理這個(gè)問題捺癞。
拒絕策略有四種:
- AbortPolicy(默認(rèn)):直接拋出RejectedExecutionException異常阻止系統(tǒng)正常運(yùn)行夷蚊。
- CallerRunsPolicy:調(diào)用者運(yùn)行。一種調(diào)節(jié)機(jī)制髓介。該策略既不會拋棄任務(wù)惕鼓,也不會拋出異常。而是將某些任務(wù)回退到調(diào)用者版保。從而降低新的任務(wù)容量呜笑。
- DiscardOldestPolicy:拋棄隊(duì)列中等待最久的任務(wù)。然后把當(dāng)前任務(wù)加入到隊(duì)列中嘗試再次提交當(dāng)前任務(wù)彻犁。
- DiscardPolicy:直接丟棄任務(wù)叫胁,不予任何處理也不拋出異常。如果允許任務(wù)丟失汞幢,這是最好的一種方案驼鹅。
在實(shí)際中,上面的三個(gè)線程池(單一的,可變的输钩,固定長度的)我們一個(gè)都不用豺型!而是一般都是自己寫。因?yàn)槟J(rèn)的這三個(gè):
FixedThreadPool和SingleThreadPool的允許請求隊(duì)列的長度(可排隊(duì)的長度)為int最大值买乃。會導(dǎo)致OOM姻氨。
CachedThreadPool和ScheduledThreadPool允許創(chuàng)建的最大線程數(shù)為int最大值。也會導(dǎo)致OOM剪验。
所以實(shí)際上我們要自己去自定義線程池肴焊。這七個(gè)參數(shù)我們也都知道意思了,隨便寫個(gè)就行了:
ExecutorService threadPool = new ThreadPoolExecutor(2, //核心線程數(shù)
5, //最大線程數(shù)
2, //空閑線程超過多久銷毀
TimeUnit.SECONDS,//空閑線程銷毀時(shí)限的單位
new LinkedBlockingQueue<Runnable>(10), //阻塞隊(duì)列功戚。能排隊(duì)的任務(wù)數(shù):10
Executors.defaultThreadFactory(),//默認(rèn)的線程工廠
new ThreadPoolExecutor.AbortPolicy());//拒絕策略娶眷。
下面測試一下拒絕策略:
我設(shè)置最大線程數(shù)5,最長隊(duì)列10.也就是同時(shí)能容納的最大數(shù)是15.然后我用十六個(gè)任務(wù)試一下:
而第二種的效果:
后兩種就是單純的扔任務(wù)還沒提示,控制臺沒什么好看的乘粒,反正知道是扔任務(wù)豌注,只不過扔的是不一樣的任務(wù)就行了。
如何合理配置最大線程數(shù)
在實(shí)際中灯萍,這幾個(gè)參數(shù)的設(shè)置是有一定的規(guī)則的幌羞。不是看心情來的。而設(shè)置的規(guī)則分兩種:
CPU密集型和IO密集型竟稳。
CPU密集型:該任務(wù)需要大量大量的運(yùn)算,而沒有阻塞熊痴,CPU一直全速運(yùn)行(CPU密集任務(wù)只有在真正的多核CPU上才可能通過多線程得到加速他爸,而在單核CPU上無論開幾個(gè)模擬的多線程都不可能得到加速。因?yàn)镃PU的運(yùn)算能力就那些)果善。
CPU密集型任務(wù)配置盡可能少的線程數(shù)量诊笤。一般公式CPU核數(shù)+1個(gè)線程的線程池。
IO密集型:由于IO密集型任務(wù)線程并不是一直在執(zhí)行任務(wù)巾陕,所以應(yīng)該配置盡可能多的線程讨跟。如CPU核數(shù)2*
IO密集型即該任務(wù)需要大量的IO,即大量的阻塞鄙煤。
在單線程上運(yùn)行IO密集型的任務(wù)會導(dǎo)致大量的CPU運(yùn)算能力浪費(fèi)在等待晾匠。所以在IO密集型任務(wù)中使用多線程可以大大的加速程序運(yùn)行。即使在單核CPU上梯刚,這種加速主要就是利用被浪費(fèi)掉的阻塞時(shí)間凉馆。公式:
CPU核數(shù)/(1-阻塞系數(shù))。阻塞系數(shù)在0.8-0.9之間。
注:上面說的是最大線程數(shù)澜共。核心線程數(shù)的話也結(jié)合實(shí)際吧向叉。稍微少點(diǎn)問題也不大。
本篇筆記就記到這里嗦董,因?yàn)槭欠至藥滋煺淼哪富眩锌赡苡械膶懙谋容^亂。反正如果稍微幫到你了記得點(diǎn)個(gè)喜歡點(diǎn)個(gè)關(guān)注京革。也祝大家工作順順利利奇唤,生活健康!