線程的那點事

線程安全概念

1.1 當多線程訪問某一個類後者方法時,都能表現出正確的行爲,那麼這個類就是線程安全的。

synchronized:可以在任意對象或者方法上加鎖,而加鎖的這段代碼稱之爲互斥區或者臨界區。

當多個線程訪問run方法時,以排隊的方式(按照cpu分配的先後順序而定)進行處理,一個線程想要執行synchronized修飾的方法裏的代碼,首先是嘗試獲得鎖,如果拿到鎖,執行synchronized中的內容,拿不到鎖這個線程就會不斷嘗試獲得這把鎖,直到拿到爲止,而且是多個線程同時爭奪這把鎖(也就是鎖競爭)。

1.2 synchronized取得鎖都是對象鎖,而不是把一段代碼當鎖,所以哪個線程先執行synchronized關鍵字的方法,哪個線程就持有該方法所屬對象的鎖,兩個對象,就獲得兩把不同的鎖,他們互不影響。

有一種情況是當synchronized修飾的方法加上static後,就會成爲類級別的鎖,表示鎖定.class類(獨佔.class類)

1.3 對象鎖的同步異步

同步:其目的就是爲了線程安全,其實對於線程安全還要滿足兩個特性:
原子性

可見性

1.4 髒讀

1.5 鎖重入

synchronized擁有鎖重入的功能,也就是在使用synchronized時,當一個線程得到了一個對象的鎖後,再次請求此對象可以再次得到該對象的鎖。

1.6 死鎖

線程間通信

線程是操作系統中獨立的個體,但這些個體如果不經過特殊處理就不能成爲整體,線程間的通信就是成爲整體的必用方式之一。可以使用wait/notify來實現線程間的通信(他們是object的方法)
1 wait和notify方法必須配合synchronized關鍵字使用

2 wait釋放鎖,notify不釋放鎖

2.2 模擬BlockingQueue

顧名思義他是一個阻塞隊列,實現LinkedBlockingQueue的put和take

put:把obj放入隊列中,如果隊列沒有空間,則調用此方法的線程被阻塞,知道隊列有了空間再繼續

take:取走隊列收個位置的對象,若隊列爲空,阻斷進入等待狀態,知道有新的元素被加入隊列。

2.3 ThreadLocal:線程局部變量,是一種線程間併發訪問變量的解決方案,與synchronized加鎖的方式不同,ThreadLocal完全不提供鎖,而使用空間換時間的概念,爲每個線程提供獨立的副本,保證線程間的安全。

從性能上說,ThreadLocal不具備有絕對的優勢,在併發不是很高時,加鎖的性能會更好,但作爲一套完全無鎖的線程解決方案,在高併發和鎖競爭激烈的場景,使用ThreadLocal可以比較好的減少鎖競爭的問題。

容器

3.1 同步類容器都是線程安全的,但某些場景下需要加鎖來保證複合操作,複合類操作如:迭代、跳轉、以及條件運算。這些複合操作在多線程併發的修改內容時,可能會出現意外情況,原因是當迭代時併發的修改了容器中的內容。

同步類容器如:HashTable,其底層無非就是用傳統的synchronized關鍵字對每個公用的方法都進行同步,使得每次只有一個線程訪問容器的狀態。這很明顯不滿足今天高併發的需求,在保證線程安全的同時,也要保證性能問題。

3.2 併發類容器

jdk5以後提供了多種併發類容器來代替同步類容器而改善性能。同步類容器的狀態都是串行化的。併發類容器是專門針對併發設計的,使用ConcurrentHashmap來代替傳統的hashtable,添加了一些常見覆合操作的支持。以及使用CopyOnWriteArrayList代替Voctor。

3.2.1 ConcurrentMap接口下有兩個重要的實現:
ConcurrentHashMap

ConcurrentSkipListMap(支持併發排序功能,彌補ConcurrentHashMap)

CocurrentHashMap內部使用段(Segment)來表示不同的部分,每個段其實就是一個小的HashTable,他們都有自己的鎖,只要多個修改操作發生在不同的段上,他們就可以併發進行。其實就是把HashMap分成了16個段(Segment),也就是最高支持16個線程的併發修改,這也是在多線程併發場景時,降低鎖競爭的一種方式,並且代碼中大多共享變量使用了Volatile關鍵字聲明,目的是第一時間獲取被修改的內容,性能非常好。

3.2.2 CopyOnWrite容器

簡稱COW,有兩種CopyOnWriteArrayList,CopyOnWriteArraySet。CopyOnWrite容器即寫時複製的容器,通俗的理解就是當我們往容器中添加一個元素時,不直接往容器中添加,而實現將當前容器進行Copy,複製出一個新的容器,然後往新的容器裏添加元素,添加完畢後,在將原容器的引用,指向新的容器。這樣做的好處是我們可以對COW容器併發的讀,而不需要加鎖,因爲當前容器不會添加任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫是不同的容器。

隊列(Queue)

4.1 ConcurrentLinkedQueue:一個適用於高併發場景下的隊列,通過無鎖的方式,實現了高併發狀態下的高性能,通常ConcurrentLinkedQueue性能好於BlockingQueue,他是一個基於鏈接節點的無界線程安全隊列。該隊列的元素遵循先進先出的原則,頭是最先加入的,尾是最近加入的,該隊列不允許NULL元素。

add(),offer()都是加入元素的方法(在ConcurrentLinkedQueue中這兩個方法沒有任何區別)

poll(),peek()都是去取頭元素節點,區別在於前者會刪除元素,後者不會。

阻塞隊列:

ArrayBlockingQueue,LinkedBlockingQueue(可轉變爲無界隊列),SynchronousQueue(同步Queue),PriorityBlockingQueue(優先級Queue)

模式

5.1 Futrue

Futrue優點類似於商品訂單,比如網購時,購買商品,提交訂單,當訂單處理完成後,在家裏等待商品送貨上門即可。也比較類似於Ajax,頁面異步處理,無需一直等待請求結果,可以繼續瀏覽頁面。

5.2 Master-Worker模式

Master-Worker模式是常用的並行計算模式。他的核心思想是系統由兩類進程協作工作:Master和Worker進程。Master負責接收和分配任務,Worker負責處理子任務。當各個Worker子進程處理完成後,會將結果返回給Master,由Master作總結歸納。其好處就是可以將一個大任務分解成若干個小任務,並行執行,從而提供系統的吞吐量。

5.3 生產者消費者

生產者和消費者也是一個非常經典的多線程模式,我們在時間開發中應用非常廣泛的思想理念。在生產消費模式中,通常有兩類線程,即若干個生產者和消費者的線程。生產者負責提交用戶請求,消費者線程負責具體處理生產者提交的任務,在生產者和消費者之間通過共享內存緩存區進行通信。

線程池

爲了更好的控制多線程,JDK提供了一套線程框架Executor,幫助開發人員有效的進行線程控制。他們都在,java.util.concurrent包中,是JDK併發包的核心。其中有一個比較重要的類Executor:他扮演着線程工廠的角色,我們通過Executors可以創建具有特定功能的線程池。

Executors創建線程池的方法:

    newCachedThreadPool()方法:返回一個可根據實際使用情況調整線程個數的線程池。不限制最大線程數量,若有空閒的線程則執行任務,若無任務則不創建線程,並且每個空閒線程默認會在60秒後自動回收。

    newFixedThreadPool()方法:返回一個固定數量的線程池,該方法的線程數始終不變。當有一個任務提交時,若線程池中有線程空閒,則立即執行,若沒有空閒線程,則被暫緩在一個任務隊列中等待有空閒的線程去執行。

    newSingleThreadExecutor()方法:創建只有一個線程的線程池。若有空閒線程則立即執行,若沒有,則被暫緩在任務隊列中。

    newScheduledThreadPool()方法:該方法返回一個ScheduledExecutorService對象,具有定時器的功能。

    自定義線程池:若Executors工廠類無法滿足我們的需求,可以自己去常見自定義線程池,其實Executors工廠類中所有的創建線程池方法均是用ThreadPoolExecutor這個類,這個類可以自定義線程,構造方法如下:

  

new ThreadPoolExecutor(int corePoolSize,                                //當前核心線程數(當線程剛new出來時線程池中線程個數)
                               int maximumPoolSize,                                 //最大線程數
                               long keepAliveTime,                                    //保持存活的時間(空閒時間)
                               TimeUnit unit,                                              //空閒時間單位
                               BlockingQueue<Runnable> workQueue    //任務隊列
                               ThreadFactory threadFactory,                     //
                               RejectedExecutionHandler handler             //拒絕執行的任務

                )    

自定義線程池使用詳細:

指定構造方法的隊列是什麼類型的比較關鍵:

    使用有界隊列時,若有新的任務需要執行,如果線程池中實際線程數小於corePoolSize,則優先創建線程;若大於corePoolSize則將該任務加入任務隊列中;若隊列已滿,則在線程總數不大於maxPoolSize的前提下創建新的線程;若線程數大於maxPoolSize則執行拒絕策略,或其他自定義方式。

    使用無界隊列時:LinkedBlockingQueue。與有界隊列相比,除非系統資源耗盡,否則無界的任務隊列不存在任務入隊失敗的問題。當有新任務到來,系統的線程數小於corePoolSize時,則新建線程執行任務;當達到corePoolSize後就不再增加新的線程。若後續還有新任務到來,而又沒有空閒的線程資源,則任務直接進入任務隊列等待。若任務創建與執行的速度差異很大,無界隊列會保持快速增長,直到耗盡系統內存。

    JKD拒絕策略:

        AbortPolicy:直接拋出異常系統正常工作

        CallerRunPolicy:只要線程池未關閉,該策略直接在調用者線程中,運行當前被丟棄的任務。

        DiscardOldestPolicy:丟棄最早的一個任務,嘗試再次提交當前任務。

        DiscardPolicy:丟棄無法處理的任務,不給予任何處理。

        其實這四種拒絕策略都比較暴力並不推薦。        

        推薦:如果需要自定義拒絕策略,可以實現RejectedExecutionHandler接口。

關於execute方法和submit方法:

    execute方法:沒有返回值,只是執行線程任務。

    submit方法:會有一個Future的返回值,該類具有get()方法,當get結果爲null時說明該線程執行完畢。

 

6.2 

CoutDwonLacth使用:用於一個線程等待多個線程的處理結果

經常用於監聽某些初始化操作,主線程可以通過await()方法直接進入阻塞狀態,其他線程進行初始化,初始化完畢後通過countdown()喚醒主線程繼續執行操作。

 

CyclicBarrier使用:用於將多個線程同時執行
假設一個場景:每個線程代表一個運動員,當運動員都準備好後才一起出發,只要有一個人沒有準備好就一起等待。

6.3 

Semaphore信號量非常適用於高併發訪問,在新系統上線之前,要對系統的訪問量進行評估。

相關概念:

PV:網站的總訪問量,頁面瀏覽量,或者點擊量,用戶每發起一次請求就會記錄一次。
UV:訪問網站的一臺電腦客戶端爲一個訪客,一般來講,0點到24點之內相同的IP客戶端只記錄一次
QPS:即每秒查詢數,qps很大程度上代表了業務的繁忙程度,每次請求的背後可能代表着多次的磁盤IO。通過QPS可以非常直觀地瞭解當前網站的訪問繁忙程度,一旦當前QPS超過預警閥值,可以考慮擴容,根據前期壓測,和後期運維才能得到準確閥值。
RT:及請求的響應時間,這個指標直接說明用戶的體驗情況。

一般來講對於系統的峯值評估採用2/8定律,即80%的請求會在20%的時間內到達,這樣我們可以根據系統對應的PV計算出峯值QPS。

峯值QPS = (總PV * 80%)/(60 * 60 * 24 *20%)

然後再將總的峯值QPS除以單臺機器所能承受的QPS值,就是所需要的服務器數量

以上不包括類似於秒殺,雙十一這種大型促銷活動的情況。

Semaphore可以控制系統的流量,拿到信號量的就進入,否則就等待。

 

其實分佈式環境中更多還是使用redis來進行限流防刷。

Lock鎖

(在1.8之前synchronized性能不如lock,8以後synchronized做了優化,性能已經差不多了,主要還是靈活性)

鎖等待與鎖通知

在使用synchronized時,如果需要多線程建進行協作工作則需要object的wait()和nitify()進行配合工作。

同樣在使用lock時,可以使用一個新的等待通知的類,他就是condition,這個condition一定是針對具體某一把鎖的,也就是只有在鎖的基礎上才能產生condition。

一把鎖可以產生多個condition,這就是lock的靈活之處

重入鎖:ReentrantLock

在需要同步的代碼部分加上鎖定,但不要忘記最後一定要釋放鎖,不然會造成鎖永遠無法釋放,其他線程永遠無法進來的情況。

公平鎖:默認是非公平鎖,公平鎖意思是哪個代碼先調用就先上鎖,非公平鎖是按照cpu分配來隨機上鎖。非公平鎖效率要高於公平鎖。

lock用法:

tryLock() 嘗試獲得鎖,獲得結果用true/false返回。
isFair() 是否是公平鎖
isLocked() 是否已經鎖定
getHoldCount() 獲得調用該lock的次數
getQueueLength() 返回正在等待獲取此鎖的線程數
hasQueuedThread(Thread thread) 查詢指定的線程是否正在等待此鎖
hasQueuedThreads() 查詢正在等待此鎖的線程

讀寫鎖:

ReentrantLockReadWriteLock

讀寫鎖核心就是實現讀寫分離的鎖,在高併發訪問下,尤其是讀多寫少的情況下,性能遠遠優於重入鎖。

讀寫鎖本質分爲兩個鎖,讀鎖和寫鎖。讀鎖下可以同時併發訪問,但在寫鎖情況下,只能一個個順序訪問。讀讀共享,寫寫互斥,讀寫互斥。

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