小夥伴們,我們認識一下。 俗世遊子:專注技術研究的程式猿 最近在做新項目的資料庫設計,目前為止一共出了80張表,預計隻做了一半,心好累o(╥﹏╥)o
前一節我們聊過了多線程的基礎問題,但是還漏掉一個知識點:
線程同步
這裡我們補上
我們總是在說多線程操作,會出現線程不安全的問題,那麼該怎麼解釋這個<code>線程安全</code>呢?
通俗的來講,當多個線程操作同一份共享資料的時候,資料的一緻性被破壞,這就是線程不安全的。
舉個例子:
循環的數值調大才能看出效果,本人試了很久
兩個線程同時操作共享變量的話,就會出現資料不一緻的問題:

那怎麼解決這個問題呢,其實也就是加鎖:<code>synchronized</code>,但是我們要注意加鎖的資源
<code>synchronized</code>是對共享變量進行加鎖,隻有線程搶占到鎖之後,該線程才能繼續操作,操作完成之後釋放鎖資源
那麼,上面的小例子我們就可以進行調整:
這樣就解決了問題,達到了我們預想的結果
那麼我們來聊一聊<code>synchronized</code>:
同步鎖,監視共享資源或共享對象(同步螢幕),需要的是Object的子類。可以通過<code>同步代碼塊</code>或者<code>同步方法</code>的方法來加鎖
同步方法也就是将業務邏輯抽離成一個普通方法,使用<code>synchronized</code>進行修飾,是一樣的效果
必須是兩個或者兩個以上的線程在同時操作同一份共享資源
使用同步鎖之後,線程隻要搶到鎖之後才能夠繼續執行,否則就必須等待鎖釋放
這種情況下執行效率可見一般
也就是說:
當線程A通路,鎖定同步螢幕,開始執行業務邏輯。當線程B通路,發現同步螢幕鎖定,無法通路
當線程A執行完成,解鎖同步螢幕,線程B通路,發現同步螢幕未鎖定,鎖定并執行業務邏輯
除了這個問題之外,線上程中中還會出現非常嚴重的問題:死鎖
死鎖,一般情況下表示互相等待,是程式運作是出現的一種狀态,簡單一點了解:
就是說兩個線程,各自需要對方的資源,但是自己又不釋放自己的資源,就造成了死鎖現象
死鎖沒辦法解決,隻能在編寫代碼的過程時刻注意
非常經典的一個案例,不知道你們在面試的時候有沒有被它支配過
前提條件:
生産者負責生産産品,放到一個區域裡,消費者從這個區域裡取走産品
關鍵點:
先生産,再消費
定義的産品類
生産者
消費者
測試方法
這裡出現兩個問題,看一下結果 出現先消費後生産的問題 出現品牌和名稱不一緻的問題
下面我們來第二版解決
首先是第一個問題:這裡我們隻需要讓生産者先執行就好,那麼如果首先執行到的是消費者,那麼就讓其等待
品牌名稱不對應的問題,這是由于生産産品和消費産品的方式不是原子性操作,在中間容易被中斷,是以我們通過同步代碼方法來解決,保證在執行過程中不會被打斷
在産品類中定義生産和消費的方法
其他不變 這樣就解決了上面的兩個問題,
其實還有一種解決方式是采用<code>BlockingQueue</code>(隊列)來解決,等待和喚醒的操作就不用我們來進行,<code>BlockingQueue</code>會幫我們來完成
就是提一下,不懂的等之後學過了隊列,就清楚了 <code>BlockingQueue</code>的版本,該類位于<code>java.util.concurrent</code>包下(JUC),後續我們詳細聊
在實際的使用中,線程是非常消耗系統資源的,而且如果對線程管理不善,很容易造成系統資源的浪費,
而且在實際開發中,會造成線程的不可控,比如:
線程名稱不統一
線程的建立方式等等
是以我們推薦在實際開發中采用線程池來進行開發,擁有以下優點:
使用線程池可以使用已有的線程來執行任務,可以避免線程在建立和銷毀時的資源消耗
由于沒有了線程的建立和銷毀過程,提高了系統的響應性能
可以通過伺服器配置對線程池進行合理的配置,比如:可運作線程數大小等
了解到這一點之後,我們來看一看其具體的實作方式,在Java中,建立線程池主要是通過<code>ThreadPoolExecutor</code>來構造,下面我們來具體了解一下
我們看參數最多的構造方法
在了解這些參數之前,我們先來聊一個知識點,就是線程池的工作原理,不然下面聊着有點生硬。畫個圖:
簡單用文字描述一下就是這樣的過程:
送出進行來的線程任務首先先判斷核心線程池中的線程是否全部都在執行任務,如果不是,那麼就建立線程執行送出進行來的線程任務,否則的話,就進入下一個判斷
判斷線程池中的阻塞隊列是否已經占滿,如果沒有,就将任務添加到阻塞隊列中等待執行,否則的話,就進行下一個判斷
這裡判斷線程池中所有的線程是否都在執行任務,如果不是,那麼就建立線程執行任務,否則的話就交給飽和政策進行處理
這是整體處理的一個過程,下面我們去實際源碼中看看:
在<code>execute()</code>的注釋中也有相當詳細的說明
好了,下面看詳細的參數,這裡非常重要
<code>corePoolSize</code>表示核心線程數,<code>maximumPoolSize</code>表示線程池中允許存在的最大線程數。
那麼
如果正在運作的線程數 小于 核心線程數,那麼當新進任務的話,即使存在空閑狀态下的線程,那麼也會建立新的線程來執行目前任務
如果正在運作的線程數 大于 核心線程數,但是 小于 最大線程數,那麼僅在等待隊列滿的時候才會建立新的線程
<code>keepAliveTime</code>:當線程數大于核心時,多餘的空閑線程在終止之前等待新任務的最長時間
<code>unit</code>表示空閑線程存活時間的表示機關
簡單一點了解:
比如現線上程池中有30個核心線程數,當任務高峰來臨時,目前核心線程數不足,那麼會新建立出20個臨時線程來執行任務,
當任務高峰結束後,發現目前30個核心線程數沒有完全在執行任務,那麼也就不會用到20個臨時線程,這20個臨時線程就是空閑的線程,然後經曆過指定的時間後,如果還沒有用到就會被銷毀
阻塞隊列或者說是等待隊列,用于存放等待執行的任務。
隊列在這裡先了解一下,等到後面聊 資料結構 的時候再詳細介紹 資料結構很重要,這裡簡單聊一下
隊列一般會和棧一起做對比,兩者都是動态集合。棧中删除的元素都是最近插入的元素,遵循的是後進先出的政策(LIFO);而隊列删除的都是在集合中存在時間最長的元素,遵循的是先進先出的政策(FIFO)。
這裡我羅列出隊列的類和說明,大家檢視一下
建立新線程時需要用到的工廠,該參數,如果沒有另外指定,則預設使用<code>Executors.defaultThreadFactory()</code> ,該工廠建立的線程全部位于同一<code>ThreadGroup</code>并且具有相同的<code>NORM_PRIORITY</code>優先級和非守護程式狀态。 通過提供其他<code>ThreadFactory</code>,可以更改線程的名稱,線程組,優先級,守護程式狀态等。
Java還為我們提供了一種工廠方式:<code>`Executors.privilegedThreadFactory()</code>。傳回用于建立具有與目前線程相同權限的新線程的線程工廠
還有一點,如果我們想自定義線程工廠的話,那麼我們可以參考上面兩種的寫法
飽和政策,也可以稱為拒絕政策。也就是當線程池中線程數都占滿了無法再繼續添加執行任務,最後就會交給飽和政策來處理
線上程池中飽和政策分為四種:
ThreadPoolExecutor.AbortPolicy
這是Java提供的預設政策,也就是說目前政策會丢棄任務并抛出<code>RejectedExecutionException</code>異常
ThreadPoolExecutor.CallerRunsPolicy
目前政策是通過調用線程處理該任務,隻要線程池不關閉,那麼就會執行該任務
ThreadPoolExecutor.DiscardPolicy
什麼都不做,直接将任務丢棄
ThreadPoolExecutor.DiscardOldestPolicy
也就是說,如果線程池沒有關閉,那麼将阻塞隊列中的頭任務丢棄,然後再通過<code>execute()</code>重新執行目前任務
在Java中,提供三種類型的線程池,下面我們一一來聊一聊
線程池執行器,為我們提供了以下幾種:
newCachedThreadPool
建立一個可根據需要建立新線程的線程池,但是在以前構造的線程可用時将重用它們,并在需要時使用提供的 ThreadFactory, 可用于業務邏輯處理時間短的操作
該方法是無參或者參數為<code>ThreadFactory</code>,其建構參數如下:
擁有以下特性:
線程池數量沒有固定,預設可以達到<code>Integer</code>最大值
線程池中的線程可進行重複利用和回收,預設時長為1分鐘
緩存隊列采用的是無緩沖的等待隊列,采用直接交接的排隊政策
寫個小案例
newFixedThreadPool
建立一個可重用固定線程數的線程池,已***隊列方式來運作這些線程
線上程池中的線程數處于一定的量,可以很好的控制線程的并發數
線程在顯示關閉之前,線程池内的線程都将一直存在
采用無限隊列的排隊政策,當核心線程中的線程繁忙時,将新任務添加到隊列中等待,在這裡,參數<code>maximumPoolSize</code>無效
寫個小案例:
newSingleThreadPoolExecutor
建立一個使用單個worker線程的Executor,已***隊列方式來運作該線程。
核心線程數為1,是以目前線程池隻會存在一個正在運作的線程任務
可保證順序的執行各個任務,并且在任意的時間點内不會存在多個活動線程
同樣采用無限隊列的排隊政策
這是一種可排程的執行器,也就是說可以執行定時任務,經常有面試會被問到
除了使用定時任務架構和Timer之外,還有什麼技術可以實作定時任務?
其中一種就是采用該線程池技術
newSingleThreadScheduledExecutor
該方法建立了一個單線程池的可排程執行器,
擁有如下特性:
核心線程數為1,之後送出的線程任務會排在隊列中一次等待執行
使用延時隊列的方式來儲存任務,隻有當其中元素指定的延遲時間到了,才能從隊列中擷取到該元素。
newScheduledThreadPool
建立一個線程池,可安排在給定延遲後運作指令或者定期執行,和上面線程池一樣,最終調用的是同一個類,但是不同點在于:
該線程池可以指定核心線程數
該線程池是JDK1.7之後添加進來的,采用了<code>分而治之</code>的思想,在<code>大資料</code>中很多地方都用到了這種思想。
建立一個帶并行級别的線程池,并行級别決定了同一個時刻做多有多少線程在執行,如不傳并行級别參數,将預設為目前系統的CPU個數
我直接給個案例吧,大家看看,畢竟這種方式本人在實際的開發中基本沒有用過
這是計算總和的例子
實作類:
RecursiveTask
通過泛型可以指定其執行的傳回結果
RecursiveAction
無傳回值
線程池生命周期隻有兩種:
RUNNING
線程池在<code>RUNNING</code>狀态下,能夠接收新的任務,并且也能夠處理阻塞隊列中的任務
TERMINATED
線程池正式進入到已終止的狀态
在這兩種狀态中間,還包含三種過度狀态:
SHUTDOWN
當線程池調用<code>shutdown()</code>方法的時候,會進入到<code>SHUTDOWN</code>狀态,該狀态下,線程池不再接收新的任務,但是阻塞隊列中的任務卻可以繼續執行
STOP
當線程池調用<code>shutdownNow()</code>方法的時候,會進入到<code>STOP</code>狀态,該狀态下,線程池不再接收新的任務,也不會執行阻塞隊列中的任務,同時還會中斷現在執行的任務
上面也就是<code>shutdown()</code>和<code>shutdownNow()</code>的差別,更多的是推薦使用<code>shutdown()</code>
TIDYING
當線程池中所有任務都已終止,并且 工作線程 為0,那麼線程池就會調用<code>terminated()</code>方法進入到TERMINATED狀态
用圖來表示:
多線程的基礎知識就聊到這裡,歡迎大家在評論區積極互動,提出自己的見解