java併發編程之多線程小結

今天這裏總的概訴一下多線程,把我們之前學習的串行起來。

多線程基礎

        線程和進行的區別: 線程是一條執行路徑。多線程是多條獨立的執行路徑,他們與進程的區別是,進程可以看作是計算機的一個獨立的應用,而線程只是一條執行路徑,一個進行會包含多個線程。

        創建線程的方式:1.繼承Thread類,重寫run方法  2.實現Runnable接口,實現run方法 總的來說就這兩種 話可以寫成匿名內部類方式。

        使用多線程的好處:合理的使用多線程可以提高程序效率,多線程會充分利用CPU,所以會壓榨CPU爲我們執行任務。但是需要合理配置線程,超出CPU承受範圍,會適得其反。

        線程的五個狀態:創建,就緒,運行,阻塞,銷燬。其中啓動線程是 start()方法。銷燬分爲兩種情況,正常執行完畢和執行期間發生異常。

        詳細瞭解請點擊我:java併發編程之多線程基礎。

        另外在--java併發編程之多線程基礎--中補充一點-------->在主線程中創建線程,這時候的線程是一個全新的執行路徑,不會受到主線程的影響,哪怕是主線程執行完畢,子線程還是會繼續執行,這個時候的子線程就相當於一條全新的執行路徑。如果多線程中涉及到事務,那麼這個時候的事務都是獨立的?切記是獨立的,不會因爲一個地方出錯而回滾。如果想要主線程銷燬,子線程也銷燬可以設置子線程爲守護線程。--------> setDaemon(true)

多線程線程安全問題

        什麼是線程安全問題:當多線程在操作同一個共享變量的時候,因爲線程不是直接操作值,而是操作的本地內存(JMM),當本地內存刷新到主內存中肯定是存在時間的,那麼如果這個時候其他線程也對主內存進行了操作,讀取不會發生線程安全問題,只有操作纔會發生線程安全問題。也就是當線程操作同一個共享變量的時候其他線程是不可見的。其他線程不知道你修改了值。

 線程的三大特性:原子性,可見性,有序性

  1. 原子性:對共享變量讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要麼執行,要麼不執行。
  2. 可見性:一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
  3. 有序性:即代碼執行順序都是重上至下串行執行。

所以,線程安全問題就是在線程修改了共享變量的時候,就是JMM和線程可見性的原因,通俗說就是當線程修改了共享變量的時候其他線程不知道。

        重排序:線程爲了提高執行效率,會把沒有依賴關係的代碼進行重新排序,就違背了有序性特性,那麼就很有可能會影響到最終的執行效果。

        volatile:該關鍵字就是用於保證可見性,和有序性的,他可以把線程修改的值馬上刷新到主內存,保證可見性,而且可以禁止從排序,但是卻不能解決線程安全問題。因爲普通的共享變量被修改之後,會在什麼時候把修改值寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證線程安全問題。

        如何避免線程安全問題:可以使用鎖機制,因爲鎖機制可以保證可見性(只允許一個線程執行)。

鎖總的來說分爲兩類:

  1. 悲觀所:是悲觀的,總認爲會發生線程安全問題,所以就加鎖。
  2. 樂觀鎖:是樂觀的,它不會認爲會發生線程安全問題,所以就不會加鎖。

多線程鎖的深入

1.悲觀鎖

悲觀鎖代表:Lock(輕量級鎖,遞歸鎖,顯示鎖,互斥鎖),synchronized關鍵字(重量級鎖,遞歸鎖,互斥鎖)

1-1:Lock接口主要常用實現類

  • ReentrantLock:可以使用 lock() 靈活的手動上鎖,但是不要忘記 ulock() 釋放鎖資源,與synchronized功能相同。
  • ReadLock:讀鎖,一次只允許一個線程進行讀取。
  • WriteLock:寫鎖,一次只允許一個線程進行寫入。

Lock鎖需要結合try進行使用,在finally中手動釋放鎖資源。

1-2:synchronized關鍵字

  • 可以是代碼塊,可以是修飾方法,可以是修飾靜態方法。
  • 如果是修飾方法,使用的是this鎖對象,如果是靜態方法,則使用的是當前類的字節碼文件。
  • 代碼塊,需要指定一個所對象,是一個對象,常用基本數據類型的包裝類型都可以作爲synchronized鎖對象。

1-3:wait,notify

    可以實現生產者消費者,當wait的時候就會被阻塞,當執行了notify纔會被釋放,必須結合synchronized使用

1-4:Condition中的await和signal

    Condition也可以達到synchronized搭配wait和notify的功能,但是Condition可以指定阻塞的時間,即到達了時間還沒執行signal放行指令,這個時候就會自動放行。

1-5:CountDownLatch計數器

    在創建該類的時候,需要指定一個int類型的初始值,該值就是執行countDown()的次數。使用改類的await()方法會阻塞線程,只有當調用了指定次數的countDown()方法,阻塞線程纔會被釋放。

1-6:CyclicBarrier屏障

    同樣會在創建的時候指定初始值,調用await()方法會阻塞線程,當之後達到指定初始值數量的線程數纔會被釋放,可以理解爲只有當指定數量線程被創建就緒之後會被統一同時釋放。

1-7:Semaphore信號量

    同樣要指定初始值,作用爲指定方法只允許指定數量的線程同時進行執行,其餘的線程會競爭拿到許可證,有了許可證纔可以執行被上鎖的方法,執行完畢之後需要歸還許可證,acquire()獲取許可證,release()釋放許可證。

CountDownLatch,CyclicBarrier,Semaphore詳細使用方法請點擊我:java併發編程之隊列

使用鎖,儘量避免鎖裏面嵌套鎖,否則容易發生死鎖現象。

2.樂觀鎖

CAS無鎖機制:Compare and Swap,即比較再交換。我們可以理解CAS無鎖機制爲一下模式

    CAS會存在三個參數,即爲:CAS(V,E,N)

  • V(Variable):需要更新的變量
  • E(Expect):預期值
  • N(New):需要更新的值

        CAS無鎖機制白話文解釋:就是當我們要更新變量(Variable)的時候,會將預期值(Expect)和我們需要修改的新值(New)進行比較,如果說Expect和New值相等,那麼就表示沒有其它線程對Variable進行修改,這個時候就會把New賦值給Variable。

樂觀鎖代表類:原子類---->java.util.concurrent.atomic包下面

這裏列出了所有的原子操作類,這裏對常用的原子類做一些介紹

原子更新基本數據類型

  • AtomicInteger:對Integer類型的原子操作。
  • AtomicBoolean:對Booleran類型原子操作。
  • AtomicLong:對Long類型的原子操作。

原子更新數組

  • AtomicIntegerArray:原子更新整形數組裏面的元素
  • AtomicLongArray:原子更新長整形數組裏面的元素
  • AtomicReferenceArray:原子更新引用類型數組裏面的元素

        另外如果使用構造函數創建上面幾個對象,並且通過構造函數傳遞需要操作的數組,那麼會賦值一份新的數組,原子不會操作之前的數組,而是操作的複製的數組。

原子更新引用類型

  • AtomicReference:原子更新引用類型
  • AtomicReferenceFieldUpdater:原子更新引用類型的字段
  • AtomicMarkableReference:原子更新帶有標記位的引用類型,會帶有版本號

原子更新字段

  • AtomicIntegerFieldUpdater:原子更新整形的字段
  • AtomicLongFieldUpdater:原子更新長整形的字段
  • AtomicStampedReference:原子更新帶有版本號的引用類型,可用於原子的更新數據和數據的版本號。
原子基本數據類型常用方法
方法名稱 作用
set(int newValue) 設置一個值,這裏是AtomicInteger,所以參數也是int類型
get() 返回當前值 什麼原子類就是返回什麼值
getAndSet(int newValue) 更新並返回更新之前的值,參數爲需要更新的值
boolean compareAndSet(int expect, int update) 更新原子操作,expect預期值,update更新的值,如果更新失敗返回false。
getAndIncrement() 自增,返回更新前的值 
getAndDecrement() 自減,返回更新前的值
incrementAndGet() 自增,返回現在的新值
decrementAndGet() 自減,返回現在的新值

黑色部分是12個原子類通用的方法。

12個原子類總結

        我們可以看到12個原子類,有兩個是紅色的,爲什麼會這樣呢?因爲其餘10個原子類也不是絕對安全的,他們有可能會發生ABA問題。

        爲什麼會發生ABA問題:前面我們說到了CAS是循環進行比較判斷,然後會把預期值和需要更新的值進行比較判斷,如果預期值等於更新的值,那麼就會進行賦值操作,那如果有一個線程修改了值,然後第二個線程又把值修改回去了。那這個時候預期值就沒有發生改變了,但是已經被修改了,這就是所謂的ABA問題。

        爲什麼AtomicStampedReferenceAtomicMarkableReference沒有ABA問題呢?因爲他在傳統的CAS(V,E,N)上增加了一個版本號,即CAS(V,E,N,V,VE),這樣只要修改了,那麼版本號都會進行改變,下次進行操作的時候就還會對版本,一級版本預期值進行判斷,如果版本不等於版本預期值,或者更新的值不等於預期值,那麼都會不允許操作。

多線程之隊列

隊列:隊列遵循LIFO原則,即先進先出,在java中隊列(Queue)跟list和set是同一個級別的,他們都是繼承於Collection接口

隊列三大類型:

  • 阻塞隊列:當隊列超中指定最大容量元素超出之後入隊會阻塞,當隊列中不存在元素時,出隊也會阻塞
  • 非阻塞隊列:顧名思義,即不會被阻塞
  • 雙端隊列:一般隊列原理爲FIFO,即先進先出原則,但是雙端隊列,頭和尾可以同時進行出隊和入隊

如果想詳細瞭解隊列,請點擊我:java併發編程之隊列

拓展:多線程如何正確中斷線程

  1. 拋出異常法 例如:int i = 1 / 0
  2. interrupt 搭配阻塞線程awit()

詳細瞭解請點擊我:interrupt() 配合阻塞中斷線程

多線程之線程池

    線程池:線程池相當於是我們多線程的一個管理者,在高併發情況下,如果頻繁的創建線程和銷燬線程,那麼會對性能有很大的影響,而且這樣線程無法管理,當線程多了之後,我們的CPU往往會喫不消,而我們的線程池就是充當一個管理者和調度者,線程池會存在一些空閒的線程,如果當前有任務過來,這個時候就不需要創建線程,直接把閒置的線程拿來使用即可,如果一下來了很多的任務,那麼這個時候線程池就會創建新的線程,如果超過了我們設置的最大線程數,那麼線程池就會把任務保存到隊列中,而且線程池中的線程在執行完畢任務之後不會被馬上銷燬,如果還有任務,那麼線程就會被複用。如果任務被處理完畢之後,被創建的線程也不會一直閒置,而是當指定時間之後會被銷燬。使用多線程,推薦使用線程池

線程池特點總結:

  1. 很友好的管理多線程。
  2. 存在閒置線程,可以達到快速響應的效果(少量請求,直接使用閒置線程,不必創建線程,當然更快)
  3. 有最大線程數量,不會存在無休止創建線程,導致程序崩潰(線程太多也不好,所以要合理配置線程
  4. 線程閒置銷燬時間,沒有使用的線程到達指定時間,那麼就會被回收銷燬(避免閒置線程消耗系統資源
  5. 有緩存隊列,超額任務可進行緩存,避免創建過多線程,和流量削鋒,避免程序崩潰
  6. 當然還可以提升小於效率咯(多線程特性
  7. 有返回值,可拋出異常

多線程兩大創建方式:

  1. 創建ThreadPoolExecutor類,調用execute方法,或者submit方法。
  2. Executors類中的抽象方法。

其中ThreadPoolExecutor類需要我們自己指定參數,進行配置線程池,Executors中的抽象方法,封裝好的,可以直接使用。

ThreadPoolExecutor

  這是ThreadPoolExecutor參數最多的一個構造函數

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
  1. corePoolSize:核心線程數,核心線程數就相當於是一直存活的空閒線程
  2. maximumPoolSize:最大線程數,允許創建最大的線程數量
  3. keepAliveTime:空閒線程存活時間
  4. unit:keepAliveTime 時間單位
  5. workQueue:當超出最大線程數之後的任務,需要被緩存的阻塞隊列
  6. threadFactory:線程創建策略
  7. handler:拒絕策略,當任務數量大於核心線程數+隊列最大容量數,就會執行的拒絕策略。

如果自定義線程池,前面五個是比較核心的參數。

Executors

  • newCachedThreadPool:一個最大線程數接近無限大的一個線程池,空餘線程回收時間爲1分鐘,如果使用這個基本上就不會發生我們上面自定義實現的 ThreadPoolExecutor 出現拒絕策略,因爲我們最大線程數才8,而這個線程池爲 Integer.MAX_VALUE 這麼大
  • newFixedThreadPool:該線程池創建時要指定最大線程數,他的最大線程數和核心線程數都是一樣的,並且如果有空餘線程會立馬被回收。但是他有一個LinkedBlockingQueue隊列,基本上也算是一個無限大的一個線程池了。
  • newScheduledThreadPool:核心線程數爲指定的線程數,但是最大線程數也是Integer.MAX_VALUE,並且支持定時執行。
  • newSingleThreadExecutor:改線程的核心線程數和最大線程數都是1,空餘線程會被立馬回收,但是隊列是LinkedBlockingQueue。

上面四個就是常用的Executors提供的四個線程池,當然Executors中的線程池遠遠不止這幾個。

execute和submit的區別

        execute:使用一個實現Runnable接口的線程作爲參數,進行執行該任務,但是沒有返回值,無法拋出異常。

        submit:使用一個實現Runnable接口或者Callable<T>接口的線程類,可以有返回值,可以拋出異常。如果接口實現Callable<T>接口,那麼就會返回一個Future接口類型,然後調用該接口中的get()方法或者get(long timeout, TimeUnit unit)方法即可拿到返回值,調用get()方法會阻塞線程,後者的區別就是當到達指定時間,如果還沒有拿到返回值,那麼就會拋出異常。

合理配置線程池

合理配置線程池非常重要,一般分爲CPU密集型,和IO密集行,有興趣的小夥伴可百度。

好了到了這裏,就大概的簡述了一下java中多線程。

往期回顧:

java併發編程之正確使用 interrupt 中斷線程

java併發編程之線程池

java併發編程之隊列

java併發編程之線程之間通訊

java併發編程之內存模型&多線程三大特性

java併發編程之線程安全問題

java併發編程之多線程基礎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章