本次的分享主要圍繞以下兩個方面:
Lambda入門
多線程技術(shù)
一悦即、Lambda入門
Lambda起源于數(shù)學(xué)中的λ演算中的一個匿名函數(shù),從它的起源我們可以知道逼裆,Lambda本身就是一個匿名函數(shù)郁稍,是Java8才推出的亮點(diǎn),體現(xiàn)了函數(shù)式編程的思想〔ǜ剑現(xiàn)在主流的編程語言都包含了函數(shù)式編程的特性艺晴,Java8在進(jìn)化過程中吸收了該特性昼钻,作為面向編程對象的補(bǔ)充。
Lambda基本語法如下圖所示封寞,Lambda語法較為簡單然评,和普通函數(shù)相比,沒有返回值以及函數(shù)名狈究,它的參數(shù)和執(zhí)行語句之間通過->連接碗淌,表示參數(shù)將傳遞到語句中執(zhí)行。Lambda表達(dá)式還有兩種簡化表達(dá)式的方法抖锥,當(dāng)表達(dá)式中只有一個執(zhí)行語句時亿眠,可以省略語句的{};如果接口的抽象方法只有一個形參磅废,()可以省略纳像,只需要參數(shù)的名稱即可。Lambda可以替代特定匿名內(nèi)部類拯勉,Lambda表達(dá)式不能單獨(dú)存在竟趾,在使用時必須繼承函數(shù)式接口。
下圖示例中的第一個Lambda表達(dá)式宫峦,形參列表的數(shù)據(jù)類型會自動推斷岔帽,只需要參數(shù)名稱。
代碼示例:小編推薦一個學(xué)JAVA的學(xué)習(xí)裙【 一三三导绷,九三零犀勒,六九三】,無論你是大牛還是小白妥曲,是想轉(zhuǎn)行還是想入行都可以來了解一起進(jìn)步一起學(xué)習(xí)贾费!裙內(nèi)有開發(fā)工具,很多干貨和技術(shù)資料分享檐盟!
在上圖展示的代碼中铸本,代碼中的匿名內(nèi)部類繼承了Flyable接口,實(shí)現(xiàn)了接口中的fly()方法遵堵。代碼準(zhǔn)備了Lambda表達(dá)式重新實(shí)現(xiàn)了Flyable接口。根據(jù)代碼中的輸出命令怨规,執(zhí)行結(jié)果顯示Lambda表達(dá)式起到了和匿名內(nèi)部類相同的作用陌宿。代碼中,并沒有定義Lambda表達(dá)式的參數(shù)類型波丰,但是我們也可以在Lambda表達(dá)式中定義符合要求的類型flyable=(int t)->System.out.println(“I can fly by Lambda”)壳坪,如果參數(shù)類型與接口中方法參數(shù)類型不一致flyable=(String t)->System.out.println(“I can fly by Lambda”),編譯器就會報(bào)錯掰烟。
假如接口實(shí)現(xiàn)了兩個方法爽蝴,匿名內(nèi)部類可以重寫新的方法沐批。但是,Lambda表達(dá)式?jīng)]法做到這一點(diǎn)蝎亚,編譯后九孩,將會提示發(fā)現(xiàn)有多個需要重寫的抽象方法。因此发框,Lambda表達(dá)式在實(shí)現(xiàn)接口時躺彬,只允許接口中有一個抽象方法,我們將這樣的接口稱為函數(shù)式接口梅惯,Java8中提供了注解@FunctionalInterface檢驗(yàn)接口是否為函數(shù)式接口宪拥,如果不是,注解將會報(bào)錯铣减。另外她君,代碼嘗試使用Lambda表達(dá)式替代抽象類的匿名內(nèi)部類的寫法,但會報(bào)錯葫哗,提示必須繼承函數(shù)式接口缔刹。因此,Lambda可以替代特定匿名內(nèi)部類魄梯,簡化代碼桨螺,但是必須繼承函數(shù)式接口。
二酿秸、多線程技術(shù)
1.進(jìn)程與線程
進(jìn)程是具有一定獨(dú)立功能的程序灭翔,關(guān)于某個數(shù)據(jù)集合上的一次運(yùn)行活動,是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個獨(dú)立單位辣苏。線程是進(jìn)程的一個實(shí)體肝箱,是CPU分配調(diào)度的基本單位,代碼的執(zhí)行體稀蟋。從概念上煌张,我們可以知道進(jìn)程是程序的一次運(yùn)行活動,需要系統(tǒng)進(jìn)行分配和調(diào)度的退客;線程是最終代碼的執(zhí)行體专控,是CPU分配調(diào)度的基本單位。同一個進(jìn)程中可以包括多個線程兜叨,并且線程共享整個進(jìn)程的資源躏率,一個進(jìn)程至少包括一個線程。如果在理解概念時很費(fèi)解茫藏,想要充分理解這些概念误趴,我們可以采用反抽象的方法,即聯(lián)系务傲,我們需要在實(shí)際生活中尋找符合概念描述的事物凉当。舉例說明:我們經(jīng)常說安卓手機(jī)比較卡枣申,手機(jī)上App跑的太多,導(dǎo)致內(nèi)存不足看杭,那么我們在手機(jī)上看到的這些App忠藤,就是一個個程序;在手機(jī)卡頓時泊窘,雙擊home鍵熄驼,看到有App在后臺運(yùn)行,這是我們看到的這些app就是進(jìn)程烘豹。進(jìn)程是需要系統(tǒng)分配資源的瓜贾,資源相當(dāng)于手機(jī)的內(nèi)存。通過這個例子携悯,我們可以加深對進(jìn)程和程序概念上的理解祭芦。另外,我們也可以通過反抽象的方法理解進(jìn)程與線程的概念憔鬼。舉例說明:公司運(yùn)轉(zhuǎn)與員工工作龟劲,這里的公司,我們可以對應(yīng)到程序轴或;進(jìn)程是程序的運(yùn)行活動昌跌,這里的進(jìn)程,我們可以理解為公司的正常運(yùn)轉(zhuǎn)照雁;同時蚕愤,公司想要正常運(yùn)轉(zhuǎn),離不開員工的工作饺蚊,員工是公司運(yùn)轉(zhuǎn)不可分割的實(shí)體萍诱,只有員工才是真正做事的人,因此我們可以將線程類比員工污呼。
小編推薦一個學(xué)JAVA的學(xué)習(xí)裙【 一三三裕坊,九三零,六九三】燕酷,無論你是大牛還是小白籍凝,是想轉(zhuǎn)行還是想入行都可以來了解一起進(jìn)步一起學(xué)習(xí)!裙內(nèi)有開發(fā)工具苗缩,很多干貨和技術(shù)資料分享静浴!
2.線程的生命周期
下圖為線程的狀態(tài)圖。所謂的生命周期挤渐,指的是線程從出生到死亡過程中,經(jīng)歷的一系列狀態(tài)双絮。線程通過創(chuàng)建Thread的一個實(shí)例new Thread()進(jìn)入new新建狀態(tài)浴麻;之后調(diào)用start()方法進(jìn)入等待被分配時間片得问,進(jìn)入runnable狀態(tài);之后软免,線程獲得CPU資源執(zhí)行任務(wù)宫纬,進(jìn)入running狀態(tài);當(dāng)線程執(zhí)行完畢或被其它線程殺死膏萧,線程就進(jìn)入dead死亡狀態(tài)漓骚;如果由于某種原因?qū)е抡谶\(yùn)行的線程讓出CPU并暫停自己的執(zhí)行,即進(jìn)入blocked堵塞狀態(tài)榛泛,在多種條件下蝌蹂,blocked狀態(tài)可以恢復(fù)成runnable狀態(tài),最終在線程重新拿到時間片后曹锨,就可以進(jìn)入running狀態(tài)重新運(yùn)行孤个。在running狀態(tài)下,如果時間片用完了或者線程主動放棄CPU的使用沛简,線程重新回到runnable狀態(tài)齐鲤。
時間片指的是CPU的時間片段,CPU將它的可執(zhí)行時間分成很多片段椒楣,每個片段隨機(jī)分配給處在runnable狀態(tài)下的線程给郊,這樣可以達(dá)到并發(fā)的效果。假設(shè)我有一個單核的CPU捧灰,通過分割很多的時間片淆九,每個程序都有機(jī)會運(yùn)行,仍然可以跑很多的程序凤壁,宏觀上看是并發(fā)的吩屹,但是由于只有一個CPU,實(shí)際上程序還是串行的拧抖。
我們可以通過閱讀JDK的Thread類注釋煤搜,創(chuàng)建并使用線程,如下圖所示唧席。
按照J(rèn)DK的注釋擦盾,下圖代碼中使用了兩種創(chuàng)建線程的方法。由于Runnable是一個函數(shù)式接口淌哟,因此代碼中使用Lambda表達(dá)式替代匿名內(nèi)部類迹卢,再將runnable傳遞給Thread,使用start()啟動線程徒仓。
上述代碼結(jié)果如下圖所示腐碱。在下圖代碼中,如果我們將t.start();替換成t.run(),打印結(jié)果將會變成:
Thread Thread run
Main runnable run.
Main
這說明run()方法并沒有真正啟動線程症见,run()方法只是在當(dāng)前的線程中執(zhí)行了run中的函數(shù)喂走。
3. 線程協(xié)作
小編推薦一個學(xué)JAVA的學(xué)習(xí)裙【 一三三,九三零谋作,六九三】芋肠,無論你是大牛還是小白,是想轉(zhuǎn)行還是想入行都可以來了解一起進(jìn)步一起學(xué)習(xí)遵蚜!裙內(nèi)有開發(fā)工具帖池,很多干貨和技術(shù)資料分享!
并行與協(xié)作:線程在并發(fā)的過程中更多的是協(xié)作關(guān)系吭净,就像之前的概念中所提到的睡汹,進(jìn)程是系統(tǒng)資源分配的單位,線程本身并沒有多少分配資源攒钳,除了維護(hù)自己必須的內(nèi)存開銷之外帮孔,線程的所有資源都是在進(jìn)程中。多線程在使用競爭中資源時不撑,存在搶占或者說是共享的關(guān)系文兢。
這時,多線程之間該如何協(xié)作焕檬,是需要我們?nèi)ソ鉀Q的姆坚。我們通過下面的代碼,學(xué)會使用關(guān)鍵字synchronized实愚,以及理解臨界區(qū)兼呵,鎖的概念。
上圖代碼模擬售票操作腊敲。一共有10張票击喂,三個售票員sellerA,seller碰辅,sellerC一起去售票懂昂,sell( )方法模擬售票行為。代碼啟動線程之后没宾,運(yùn)行結(jié)果如下圖所示凌彬。售票員sellerA在一個時間片內(nèi)將sell方法中的代碼全部跑完,票售空循衰,但是sellerB與sellerC在線程并發(fā)時铲敛,也售出了第10張票,存在重復(fù)售票会钝,這樣的操作是不合理的伐蒋。
為了解決重復(fù)售票的問題,我們可以使用Java中提供的同步關(guān)鍵字synchronized修飾sell( )方法,代碼如下圖所示先鱼。使用關(guān)鍵字synchronized修飾后徒蟆,多線程在訪問sell( )方法時,能保證只有一個線程執(zhí)行這個方法型型,當(dāng)前線程執(zhí)行完sell( )方法后,其他線程才能執(zhí)行sell( )方法全蝶。
執(zhí)行上述代碼后闹蒜,輸出結(jié)果如下圖所示。從下面結(jié)果可以看到抑淫,代碼解決了重復(fù)售票的不合理問題绷落,但是仍然只有sellerA一個在售票。原因在于始苇,通過關(guān)鍵字synchronized修飾sell( )方法后砌烁,sellerA在拿到sell( )方法的執(zhí)行權(quán)時,把里面的代碼一口氣執(zhí)行完了催式,也就是將票全部賣出函喉,等sellerA執(zhí)行完后,sellerB和sellerC再執(zhí)行sell( )方法時荣月,票數(shù)已經(jīng)為0管呵,自然會出現(xiàn)下圖中沒有賣出一張票的現(xiàn)象。我們將方法sell( )中的內(nèi)容叫做臨界區(qū)哺窄,當(dāng)一個線程進(jìn)入臨界區(qū)后捐下,其他線程必須等待該線程執(zhí)行完臨界區(qū)內(nèi)容后,才能進(jìn)入該臨界區(qū)萌业。
下圖所示的代碼改善了上述sellerA一口氣賣完所有票的現(xiàn)象坷襟。代碼在方法體內(nèi)使用關(guān)鍵字synchronized,括號中的this表示一個對象或者一個類生年。代碼相較于上面的解決方法婴程,將臨界區(qū)從整個方法縮小到兩行代碼。也就是說多線程在執(zhí)行這兩行代碼時是同步的晶框。
上圖代碼執(zhí)行結(jié)果如下圖所示排抬。從圖中我們可以發(fā)現(xiàn),不再是只有sellerA在賣票授段。并且代碼每次執(zhí)行結(jié)果都是不一樣的蹲蒲,因?yàn)镃PU的時間片是隨機(jī)給出的。上述代碼中的try catch方法塊使線程睡50ms侵贵,延長售票操作的時間届搁,在這段時間內(nèi)可以執(zhí)行其他的操作(比如,將該票給某個顧客)。代碼改善過后卡睦,保證資源不是被獨(dú)占的宴胧,使資源分配均勻。
從上圖我們發(fā)現(xiàn)表锻,存在無效票恕齐,原因在于:假設(shè)當(dāng)前票數(shù)為1,A進(jìn)入臨界區(qū)售票瞬逊,而此時B已經(jīng)進(jìn)行判斷显歧,在臨界區(qū)外等待了。當(dāng)A賣完票后确镊,票數(shù)為0士骤,但是B還是會進(jìn)入臨界區(qū)進(jìn)行售票操作,因此蕾域,出現(xiàn)無效票-1的情況拷肌。這說明代碼需要進(jìn)一步改善。改善后的代碼如下圖所示旨巷。代碼在臨界區(qū)內(nèi)加入判斷條件巨缘,只有票數(shù)大于0時,才會進(jìn)行售票操作契沫,這是常用的雙重檢驗(yàn)方法带猴。經(jīng)過雙重檢驗(yàn)后,運(yùn)行代碼就不會出現(xiàn)無效售票懈万。
下面介紹另外一種單線程同步的方法拴清。代碼如下圖所示。代碼通過Lock接口定義了一個鎖会通,使用ReentrantLock實(shí)現(xiàn)口予。鎖和上面提到的關(guān)鍵字synchronized作用是一樣的,都是定義出一個臨界區(qū)涕侈,讓線程進(jìn)入臨界區(qū)時實(shí)現(xiàn)線程同步沪停。代碼通過lock.lock( )定義臨界區(qū)的初始點(diǎn),使用在try語句塊中定義臨界區(qū)執(zhí)行內(nèi)容, finally語句塊中采用unlock( )方法進(jìn)行解鎖裳涛。在unlock后線程才算真正走出臨界區(qū)木张。使用try,finally的原因在于:如果try中拋出異常端三,如果沒有finally中的解鎖舷礼,線程不會調(diào)用unlock方法,永遠(yuǎn)占用這把鎖郊闯,導(dǎo)致其他線程無法進(jìn)入臨界區(qū)執(zhí)行代碼妻献。在finally中調(diào)用unlock( )方法保證無論什么情況下蛛株,鎖終將被釋放。避免死鎖育拨。
上圖中的代碼谨履,如果線程遇到售賣同一張票,鎖沒有被釋放熬丧,線程將會等待笋粟。改善這種情況的方法是,我們使用10把鎖析蝴,使得每張票都有一把鎖矗钟,當(dāng)線程A售賣某張票時,其他線程可以跳過這張票嫌变,無需等待去賣其他未售出的票」或者腾啥,使用兩把鎖,五張票一把鎖冯吓,這種分段鎖的策略進(jìn)一步提高了并發(fā)的效率倘待。
4. 線程池
線程雖然不占用進(jìn)程中的資源,但在Java中组贺,如果每當(dāng)一個請求到達(dá)就創(chuàng)建一個新線程凸舵,開銷是相當(dāng)大的。并且失尖,如果在一個JVM里創(chuàng)建太多的線程啊奄,可能會導(dǎo)致系統(tǒng)由于過度消耗內(nèi)存導(dǎo)致系統(tǒng)資源不足,為了防止資源不足掀潮,應(yīng)該盡可能減少創(chuàng)建和銷毀線程的次數(shù)菇夸,特別是一些資源耗費(fèi)比較大的線程的創(chuàng)建和銷毀,盡量復(fù)用已有對象來進(jìn)行服務(wù)仪吧,這就線程池技術(shù)產(chǎn)生的原因庄新。如果想要實(shí)現(xiàn)線程的復(fù)用,我們需要繼承線程薯鼠,在run方法中通過循環(huán)不斷從外部獲取runnable的實(shí)現(xiàn)择诈,以此達(dá)到線程復(fù)用的目的。有了復(fù)用后出皇,可以提供線程池羞芍,管理線程,線程池可以控制線程的并發(fā)度恶迈,同時涩金,通過對多個任務(wù)重用線程谱醇,線程創(chuàng)建的開銷就被分?jǐn)偟搅硕鄠€任務(wù)上了,而且由于在請求到達(dá)時線程已經(jīng)存在步做,所以消除了線程創(chuàng)建所帶來的延遲副渴。
下面介紹一下線程池的使用。下圖代碼中展示了ThreadPoolExecutor的構(gòu)造方法全度,下面介紹一下方法中包含的參數(shù)煮剧。
corePoolSize:表示線程池的核心線程數(shù),指線程池中常駐線程的數(shù)量将鸵,核心線程數(shù)會一直在線程池中存活勉盅,除非線程池停止使用被資源回收了。
maximumPoolSize:指線程池所能容納的最大線程數(shù)量顶掉,當(dāng)活動線程數(shù)到達(dá)這個數(shù)值后草娜,后續(xù)的新任務(wù)將會被阻塞。
keepAliveTime:非核心線程閑置時的超時時長痒筒,超過這個時長宰闰,非核心線程就會被回收。當(dāng)ThreadPoolExecutor的allowCoreThreadTimeOut屬性設(shè)置為true時簿透,keepAliveTime同樣會作用于核心線程移袍。
Unit:用于指定keepAliveTime參數(shù)的時間單位。
workQueue:表示線程池中的任務(wù)隊(duì)列(阻塞隊(duì)列)老充,通過線程池的execute方法提交Runnable對象會存儲在這個隊(duì)列中葡盗。
threadFactory:表示線程工廠,為線程池提供創(chuàng)建新線程的功能啡浊。
RejectExecutionHandler:這個參數(shù)表示當(dāng)ThreadPoolExecutor已經(jīng)關(guān)閉或者已經(jīng)飽和時(達(dá)到了最大線程池大小而且工作隊(duì)列已經(jīng)滿)觅够,提供以下幾個策略考慮是否拒絕到達(dá)的任務(wù)。DiscardPolicy:直接忽略提交的任務(wù)
AbortPolicy:忽略提交的任務(wù)巷嚣,在拒絕的同時拋出異常蔚约,通知調(diào)用者拒絕執(zhí)行
CallerRunsPolicy:讓線程池的使用者所在的線程運(yùn)行提交的任務(wù)調(diào)用者
DiscardOlderestPolicy:忽略最早放到隊(duì)列中的任務(wù)
下圖代碼中自定義了一個線程池。通過線程池的submit( )方法提交runnable的實(shí)現(xiàn)涂籽,最終通過線程池的shutdown( )方法關(guān)閉線程池苹祟。
Java包中預(yù)置的線程池有以下幾種:newSingleThreadExecutor;newFixedThreadPool:newCachedThreadPool: newScheduledThreadPool: 但在阿里巴巴的Java開發(fā)中是不建議甚至禁止使用Java預(yù)置線程池的评雌。下圖中的代碼目的是尋找SingleThreadExecutor的bug树枫。
小編推薦一個學(xué)JAVA的學(xué)習(xí)裙【 一三三,九三零景东,六九三】砂轻,無論你是大牛還是小白,是想轉(zhuǎn)行還是想入行都可以來了解一起進(jìn)步一起學(xué)習(xí)斤吐!裙內(nèi)有開發(fā)工具搔涝,很多干貨和技術(shù)資料分享厨喂!上述代碼的運(yùn)行結(jié)果如下圖所示。代碼利用循環(huán)庄呈,無限添加runnable的實(shí)現(xiàn)蜕煌,但是由于單一線程的阻塞隊(duì)列是沒有邊界的,會導(dǎo)致添加的對象過多诬留,耗盡內(nèi)存資源斜纪。因此阿里巴巴開發(fā)手冊是明確禁止使用Java預(yù)置線程池的。
如果對JAVA微服務(wù)文兑、分布式盒刚、高并發(fā)、高可用绿贞、大型互聯(lián)網(wǎng)架構(gòu)技術(shù)因块、面試經(jīng)驗(yàn)交流。小編推薦一個學(xué)JAVA的學(xué)習(xí)裙【 一三三籍铁,九三零贮聂,六九三】,無論你是大牛還是小白寨辩,是想轉(zhuǎn)行還是想入行都可以來了解一起進(jìn)步一起學(xué)習(xí)!裙內(nèi)有開發(fā)工具歼冰,很多干貨和技術(shù)資料分享靡狞!,這些資料都是從各個技術(shù)網(wǎng)站搜集隔嫡、整理出來的甸怕,如果你有好的學(xué)習(xí)資料可以私聊發(fā)我,我會注明出處之后分享給大家腮恩。歡迎分享梢杭,歡迎評論,歡迎轉(zhuǎn)發(fā)秸滴!