阿里專家與你分享:你必須瞭解的Java多線程技術!

本次的分享主要圍繞以下兩個方面:

Lambda入門

多線程技術

一、Lambda入門

Lambda起源於數學中的λ演算中的一個匿名函數,從它的起源我們可以知道,Lambda本身就是一個匿名函數,是Java8才推出的亮點,體現了函數式編程的思想。現在主流的編程語言都包含了函數式編程的特性,Java8在進化過程中吸收了該特性,作為面向編程對象的補充。

Lambda基本語法如下圖所示,Lambda語法較為簡單,和普通函數相比,沒有返回值以及函數名,它的參數和執行語句之間通過->連接,表示參數將傳遞到語句中執行。Lambda表達式還有兩種簡化表達式的方法,當表達式中只有一個執行語句時,可以省略語句的{};如果接口的抽象方法只有一個形參,()可以省略,只需要參數的名稱即可。Lambda可以替代特定匿名內部類,Lambda表達式不能單獨存在,在使用時必須繼承函數式接口。

下圖示例中的第一個Lambda表達式,形參列表的數據類型會自動推斷,只需要參數名稱。

阿里專家與你分享:你必須瞭解的Java多線程技術!

代碼示例:

阿里專家與你分享:你必須瞭解的Java多線程技術!

阿里專家與你分享:你必須瞭解的Java多線程技術!

在上圖展示的代碼中,代碼中的匿名內部類繼承了Flyable接口,實現了接口中的fly()方法。代碼準備了Lambda表達式重新實現了Flyable接口。根據代碼中的輸出命令,執行結果顯示Lambda表達式起到了和匿名內部類相同的作用。代碼中,並沒有定義Lambda表達式的參數類型,但是我們也可以在Lambda表達式中定義符合要求的類型flyable=(int t)->System.out.println(“I can fly by Lambda”),如果參數類型與接口中方法參數類型不一致flyable=(String t)->System.out.println(“I can fly by Lambda”),編譯器就會報錯。

假如接口實現了兩個方法,匿名內部類可以重寫新的方法。但是,Lambda表達式沒法做到這一點,編譯後,將會提示發現有多個需要重寫的抽象方法。因此,Lambda表達式在實現接口時,只允許接口中有一個抽象方法,我們將這樣的接口稱為函數式接口,Java8中提供了註解@FunctionalInterface檢驗接口是否為函數式接口,如果不是,註解將會報錯。另外,代碼嘗試使用Lambda表達式替代抽象類的匿名內部類的寫法,但會報錯,提示必須繼承函數式接口。因此,Lambda可以替代特定匿名內部類,簡化代碼,但是必須繼承函數式接口。

二、多線程技術

1.進程與線程

進程是具有一定獨立功能的程序,關於某個數據集合上的一次運行活動,是系統進行資源分配和調度的一個獨立單位。線程是進程的一個實體,是CPU分配調度的基本單位,代碼的執行體。從概念上,我們可以知道進程是程序的一次運行活動,需要系統進行分配和調度的;線程是最終代碼的執行體,是CPU分配調度的基本單位。同一個進程中可以包括多個線程,並且線程共享整個進程的資源,一個進程至少包括一個線程。如果在理解概念時很費解,想要充分理解這些概念,我們可以採用反抽象的方法,即聯繫,我們需要在實際生活中尋找符合概念描述的事物。舉例說明:我們經常說安卓手機比較卡,手機上App跑的太多,導致內存不足,那麼我們在手機上看到的這些App,就是一個個程序;在手機卡頓時,雙擊home鍵,看到有App在後臺運行,這是我們看到的這些app就是進程。進程是需要系統分配資源的,資源相當於手機的內存。通過這個例子,我們可以加深對進程和程序概念上的理解。另外,我們也可以通過反抽象的方法理解進程與線程的概念。舉例說明:公司運轉與員工工作,這裡的公司,我們可以對應到程序;進程是程序的運行活動,這裡的進程,我們可以理解為公司的正常運轉;同時,公司想要正常運轉,離不開員工的工作,員工是公司運轉不可分割的實體,只有員工才是真正做事的人,因此我們可以將線程類比員工。

2.線程的生命週期

下圖為線程的狀態圖。所謂的生命週期,指的是線程從出生到死亡過程中,經歷的一系列狀態。線程通過創建Thread的一個實例new Thread()進入new新建狀態;之後調用start()方法進入等待被分配時間片,進入runnable狀態;之後,線程獲得CPU資源執行任務,進入running狀態;當線程執行完畢或被其它線程殺死,線程就進入dead死亡狀態;如果由於某種原因導致正在運行的線程讓出CPU並暫停自己的執行,即進入blocked堵塞狀態,在多種條件下,blocked狀態可以恢復成runnable狀態,最終在線程重新拿到時間片後,就可以進入running狀態重新運行。在running狀態下,如果時間片用完了或者線程主動放棄CPU的使用,線程重新回到runnable狀態。

時間片指的是CPU的時間片段,CPU將它的可執行時間分成很多片段,每個片段隨機分配給處在runnable狀態下的線程,這樣可以達到併發的效果。假設我有一個單核的CPU,通過分割很多的時間片,每個程序都有機會運行,仍然可以跑很多的程序,宏觀上看是併發的,但是由於只有一個CPU,實際上程序還是串行的。

阿里專家與你分享:你必須瞭解的Java多線程技術!

我們可以通過閱讀JDK的Thread類註釋,創建並使用線程,如下圖所示。

阿里專家與你分享:你必須瞭解的Java多線程技術!

按照JDK的註釋,下圖代碼中使用了兩種創建線程的方法。由於Runnable是一個函數式接口,因此代碼中使用Lambda表達式替代匿名內部類,再將runnable傳遞給Thread,使用start()啟動線程。

阿里專家與你分享:你必須瞭解的Java多線程技術!

上述代碼結果如下圖所示。在下圖代碼中,如果我們將t.start();替換成t.run(),打印結果將會變成:

Thread Thread run

Main runnable run.

Main

這說明run()方法並沒有真正啟動線程,run()方法只是在當前的線程中執行了run中的函數。

阿里專家與你分享:你必須瞭解的Java多線程技術!

3. 線程協作

並行與協作:線程在併發的過程中更多的是協作關係,就像之前的概念中所提到的,進程是系統資源分配的單位,線程本身並沒有多少分配資源,除了維護自己必須的內存開銷之外,線程的所有資源都是在進程中。多線程在使用競爭中資源時,存在搶佔或者說是共享的關係。

這時,多線程之間該如何協作,是需要我們去解決的。我們通過下面的代碼,學會使用關鍵字synchronized,以及理解臨界區,鎖的概念。

阿里專家與你分享:你必須瞭解的Java多線程技術!

上圖代碼模擬售票操作。一共有10張票,三個售票員sellerA,seller,sellerC一起去售票,sell( )方法模擬售票行為。代碼啟動線程之後,運行結果如下圖所示。售票員sellerA在一個時間片內將sell方法中的代碼全部跑完,票售空,但是sellerB與sellerC在線程併發時,也售出了第10張票,存在重複售票,這樣的操作是不合理的。

阿里專家與你分享:你必須瞭解的Java多線程技術!

為了解決重複售票的問題,我們可以使用Java中提供的同步關鍵字synchronized修飾sell( )方法,代碼如下圖所示。使用關鍵字synchronized修飾後,多線程在訪問sell( )方法時,能保證只有一個線程執行這個方法,當前線程執行完sell( )方法後,其他線程才能執行sell( )方法。

阿里專家與你分享:你必須瞭解的Java多線程技術!

執行上述代碼後,輸出結果如下圖所示。從下面結果可以看到,代碼解決了重複售票的不合理問題,但是仍然只有sellerA一個在售票。原因在於,通過關鍵字synchronized修飾sell( )方法後,sellerA在拿到sell( )方法的執行權時,把裡面的代碼一口氣執行完了,也就是將票全部賣出,等sellerA執行完後,sellerB和sellerC再執行sell( )方法時,票數已經為0,自然會出現下圖中沒有賣出一張票的現象。我們將方法sell( )中的內容叫做臨界區,當一個線程進入臨界區後,其他線程必須等待該線程執行完臨界區內容後,才能進入該臨界區。

阿里專家與你分享:你必須瞭解的Java多線程技術!

下圖所示的代碼改善了上述sellerA一口氣賣完所有票的現象。代碼在方法體內使用關鍵字synchronized,括號中的this表示一個對象或者一個類。代碼相較於上面的解決方法,將臨界區從整個方法縮小到兩行代碼。也就是說多線程在執行這兩行代碼時是同步的。

阿里專家與你分享:你必須瞭解的Java多線程技術!

上圖代碼執行結果如下圖所示。從圖中我們可以發現,不再是隻有sellerA在賣票。並且代碼每次執行結果都是不一樣的,因為CPU的時間片是隨機給出的。上述代碼中的try catch方法塊使線程睡50ms,延長售票操作的時間,在這段時間內可以執行其他的操作(比如,將該票給某個顧客)。代碼改善過後,保證資源不是被獨佔的,使資源分配均勻。

阿里專家與你分享:你必須瞭解的Java多線程技術!

從上圖我們發現,存在無效票,原因在於:假設當前票數為1,A進入臨界區售票,而此時B已經進行判斷,在臨界區外等待了。當A賣完票後,票數為0,但是B還是會進入臨界區進行售票操作,因此,出現無效票-1的情況。這說明代碼需要進一步改善。改善後的代碼如下圖所示。代碼在臨界區內加入判斷條件,只有票數大於0時,才會進行售票操作,這是常用的雙重檢驗方法。經過雙重檢驗後,運行代碼就不會出現無效售票。

阿里專家與你分享:你必須瞭解的Java多線程技術!

下面介紹另外一種單線程同步的方法。代碼如下圖所示。代碼通過Lock接口定義了一個鎖,使用ReentrantLock實現。鎖和上面提到的關鍵字synchronized作用是一樣的,都是定義出一個臨界區,讓線程進入臨界區時實現線程同步。代碼通過lock.lock( )定義臨界區的初始點,使用在try語句塊中定義臨界區執行內容, finally語句塊中採用unlock( )方法進行解鎖。在unlock後線程才算真正走出臨界區。使用try,finally的原因在於:如果try中拋出異常,如果沒有finally中的解鎖,線程不會調用unlock方法,永遠佔用這把鎖,導致其他線程無法進入臨界區執行代碼。在finally中調用unlock( )方法保證無論什麼情況下,鎖終將被釋放。避免死鎖。

阿里專家與你分享:你必須瞭解的Java多線程技術!

上圖中的代碼,如果線程遇到售賣同一張票,鎖沒有被釋放,線程將會等待。改善這種情況的方法是,我們使用10把鎖,使得每張票都有一把鎖,當線程A售賣某張票時,其他線程可以跳過這張票,無需等待去賣其他未售出的票。或者,使用兩把鎖,五張票一把鎖,這種分段鎖的策略進一步提高了併發的效率。

4. 線程池

線程雖然不佔用進程中的資源,但在Java中,如果每當一個請求到達就創建一個新線程,開銷是相當大的。並且,如果在一個JVM裡創建太多的線程,可能會導致系統由於過度消耗內存導致系統資源不足,為了防止資源不足,應該儘可能減少創建和銷燬線程的次數,特別是一些資源耗費比較大的線程的創建和銷燬,儘量複用已有對象來進行服務,這就線程池技術產生的原因。如果想要實現線程的複用,我們需要繼承線程,在run方法中通過循環不斷從外部獲取runnable的實現,以此達到線程複用的目的。有了複用後,可以提供線程池,管理線程,線程池可以控制線程的併發度,同時,通過對多個任務重用線程,線程創建的開銷就被分攤到了多個任務上了,而且由於在請求到達時線程已經存在,所以消除了線程創建所帶來的延遲。

下面介紹一下線程池的使用。下圖代碼中展示了ThreadPoolExecutor的構造方法,下面介紹一下方法中包含的參數。

  • corePoolSize:表示線程池的核心線程數,指線程池中常駐線程的數量,核心線程數會一直在線程池中存活,除非線程池停止使用被資源回收了。
  • maximumPoolSize:指線程池所能容納的最大線程數量,當活動線程數到達這個數值後,後續的新任務將會被阻塞。
  • keepAliveTime:非核心線程閒置時的超時時長,超過這個時長,非核心線程就會被回收。當ThreadPoolExecutor的allowCoreThreadTimeOut屬性設置為true時,keepAliveTime同樣會作用於核心線程。
  • Unit:用於指定keepAliveTime參數的時間單位。
  • workQueue:表示線程池中的任務隊列(阻塞隊列),通過線程池的execute方法提交Runnable對象會存儲在這個隊列中。
  • threadFactory:表示線程工廠,為線程池提供創建新線程的功能。
  • RejectExecutionHandler:這個參數表示當ThreadPoolExecutor已經關閉或者已經飽和時(達到了最大線程池大小而且工作隊列已經滿),提供以下幾個策略考慮是否拒絕到達的任務。DiscardPolicy:直接忽略提交的任務
  • AbortPolicy:忽略提交的任務,在拒絕的同時拋出異常,通知調用者拒絕執行
  • CallerRunsPolicy:讓線程池的使用者所在的線程運行提交的任務調用者
  • DiscardOlderestPolicy:忽略最早放到隊列中的任務
阿里專家與你分享:你必須瞭解的Java多線程技術!

下圖代碼中自定義了一個線程池。通過線程池的submit( )方法提交runnable的實現,最終通過線程池的shutdown( )方法關閉線程池。

阿里專家與你分享:你必須瞭解的Java多線程技術!

Java包中預置的線程池有以下幾種:newSingleThreadExecutor;newFixedThreadPool:newCachedThreadPool: newScheduledThreadPool: 但在阿里巴巴的Java開發中是不建議甚至禁止使用Java預置線程池的。下圖中的代碼目的是尋找SingleThreadExecutor的bug。

阿里專家與你分享:你必須瞭解的Java多線程技術!

上述代碼的運行結果如下圖所示。代碼利用循環,無限添加runnable的實現,但是由於單一線程的阻塞隊列是沒有邊界的,會導致添加的對象過多,耗盡內存資源。因此阿里巴巴開發手冊是明確禁止使用Java預置線程池的。

阿里專家與你分享:你必須瞭解的Java多線程技術!


分享到:


相關文章: