今天這裏總的概訴一下多線程,把我們之前學習的串行起來。
多線程基礎
線程和進行的區別: 線程是一條執行路徑。多線程是多條獨立的執行路徑,他們與進程的區別是,進程可以看作是計算機的一個獨立的應用,而線程只是一條執行路徑,一個進行會包含多個線程。
創建線程的方式:1.繼承Thread類,重寫run方法 2.實現Runnable接口,實現run方法 總的來說就這兩種 話可以寫成匿名內部類方式。
使用多線程的好處:合理的使用多線程可以提高程序效率,多線程會充分利用CPU,所以會壓榨CPU爲我們執行任務。但是需要合理配置線程,超出CPU承受範圍,會適得其反。
線程的五個狀態:創建,就緒,運行,阻塞,銷燬。其中啓動線程是 start()方法。銷燬分爲兩種情況,正常執行完畢和執行期間發生異常。
詳細瞭解請點擊我:java併發編程之多線程基礎。
另外在--java併發編程之多線程基礎--中補充一點-------->在主線程中創建線程,這時候的線程是一個全新的執行路徑,不會受到主線程的影響,哪怕是主線程執行完畢,子線程還是會繼續執行,這個時候的子線程就相當於一條全新的執行路徑。如果多線程中涉及到事務,那麼這個時候的事務都是獨立的?切記是獨立的,不會因爲一個地方出錯而回滾。如果想要主線程銷燬,子線程也銷燬可以設置子線程爲守護線程。--------> setDaemon(true)
多線程線程安全問題
什麼是線程安全問題:當多線程在操作同一個共享變量的時候,因爲線程不是直接操作值,而是操作的本地內存(JMM),當本地內存刷新到主內存中肯定是存在時間的,那麼如果這個時候其他線程也對主內存進行了操作,讀取不會發生線程安全問題,只有操作纔會發生線程安全問題。也就是當線程操作同一個共享變量的時候其他線程是不可見的。其他線程不知道你修改了值。
線程的三大特性:原子性,可見性,有序性
- 原子性:對共享變量讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要麼執行,要麼不執行。
- 可見性:一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
- 有序性:即代碼執行順序都是重上至下串行執行。
所以,線程安全問題就是在線程修改了共享變量的時候,就是JMM和線程可見性的原因,通俗說就是當線程修改了共享變量的時候其他線程不知道。
重排序:線程爲了提高執行效率,會把沒有依賴關係的代碼進行重新排序,就違背了有序性特性,那麼就很有可能會影響到最終的執行效果。
volatile:該關鍵字就是用於保證可見性,和有序性的,他可以把線程修改的值馬上刷新到主內存,保證可見性,而且可以禁止從排序,但是卻不能解決線程安全問題。因爲普通的共享變量被修改之後,會在什麼時候把修改值寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證線程安全問題。
如何避免線程安全問題:可以使用鎖機制,因爲鎖機制可以保證可見性(只允許一個線程執行)。
鎖總的來說分爲兩類:
- 悲觀所:是悲觀的,總認爲會發生線程安全問題,所以就加鎖。
- 樂觀鎖:是樂觀的,它不會認爲會發生線程安全問題,所以就不會加鎖。
多線程鎖的深入
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問題。
爲什麼AtomicStampedReference和AtomicMarkableReference沒有ABA問題呢?因爲他在傳統的CAS(V,E,N)上增加了一個版本號,即CAS(V,E,N,V,VE),這樣只要修改了,那麼版本號都會進行改變,下次進行操作的時候就還會對版本,一級版本預期值進行判斷,如果版本不等於版本預期值,或者更新的值不等於預期值,那麼都會不允許操作。
多線程之隊列
隊列:隊列遵循LIFO原則,即先進先出,在java中隊列(Queue)跟list和set是同一個級別的,他們都是繼承於Collection接口
隊列三大類型:
- 阻塞隊列:當隊列超中指定最大容量元素超出之後入隊會阻塞,當隊列中不存在元素時,出隊也會阻塞
- 非阻塞隊列:顧名思義,即不會被阻塞
- 雙端隊列:一般隊列原理爲FIFO,即先進先出原則,但是雙端隊列,頭和尾可以同時進行出隊和入隊
如果想詳細瞭解隊列,請點擊我:java併發編程之隊列
拓展:多線程如何正確中斷線程
- 拋出異常法 例如:int i = 1 / 0
- interrupt 搭配阻塞線程awit()
詳細瞭解請點擊我:interrupt() 配合阻塞中斷線程
多線程之線程池
線程池:線程池相當於是我們多線程的一個管理者,在高併發情況下,如果頻繁的創建線程和銷燬線程,那麼會對性能有很大的影響,而且這樣線程無法管理,當線程多了之後,我們的CPU往往會喫不消,而我們的線程池就是充當一個管理者和調度者,線程池會存在一些空閒的線程,如果當前有任務過來,這個時候就不需要創建線程,直接把閒置的線程拿來使用即可,如果一下來了很多的任務,那麼這個時候線程池就會創建新的線程,如果超過了我們設置的最大線程數,那麼線程池就會把任務保存到隊列中,而且線程池中的線程在執行完畢任務之後不會被馬上銷燬,如果還有任務,那麼線程就會被複用。如果任務被處理完畢之後,被創建的線程也不會一直閒置,而是當指定時間之後會被銷燬。使用多線程,推薦使用線程池
線程池特點總結:
- 很友好的管理多線程。
- 存在閒置線程,可以達到快速響應的效果(少量請求,直接使用閒置線程,不必創建線程,當然更快)
- 有最大線程數量,不會存在無休止創建線程,導致程序崩潰(線程太多也不好,所以要合理配置線程
- 線程閒置銷燬時間,沒有使用的線程到達指定時間,那麼就會被回收銷燬(避免閒置線程消耗系統資源
- 有緩存隊列,超額任務可進行緩存,避免創建過多線程,和流量削鋒,避免程序崩潰
- 當然還可以提升小於效率咯(多線程特性
- 有返回值,可拋出異常
多線程兩大創建方式:
- 創建ThreadPoolExecutor類,調用execute方法,或者submit方法。
- Executors類中的抽象方法。
其中ThreadPoolExecutor類需要我們自己指定參數,進行配置線程池,Executors中的抽象方法,封裝好的,可以直接使用。
ThreadPoolExecutor
這是ThreadPoolExecutor參數最多的一個構造函數
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
- corePoolSize:核心線程數,核心線程數就相當於是一直存活的空閒線程
- maximumPoolSize:最大線程數,允許創建最大的線程數量
- keepAliveTime:空閒線程存活時間
- unit:keepAliveTime 時間單位
- workQueue:當超出最大線程數之後的任務,需要被緩存的阻塞隊列
- threadFactory:線程創建策略
- 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中多線程。
往期回顧: