注:此博文為本人學習過程中的筆記
1.常見的鎖策略
當我們需要自己實現一把鎖時,需要關注鎖策略。Java提供的synchronized已經非常好用了,覆蓋了絕大多數的使用場景。此處的鎖策略并不是和Java強相關的,只要涉及到并發編程,涉及到鎖,都要關注鎖策略。鎖策略就是指這個鎖在加鎖的過程中有什么特點,有什么行為
1.樂觀鎖和悲觀鎖
這不是針對某一把具體的鎖,而是一種特性,一把鎖具有“樂觀”或者“悲觀”的特性。
悲觀鎖是指加鎖的時候,預測接下來的鎖競爭非常激烈,針對這樣激烈的情況會做一些額外的工作。
樂觀鎖是指加鎖的時候,預測接下來的鎖競爭不激烈,不做額外的工作。
2.重量級鎖和輕量級鎖
這是指遇到以上場景之后的解決方案
重量級鎖是指當悲觀的場景下付出更多的代價,更低效
輕量級鎖是指當樂觀的場景下付出較少的代價,更高效
3.掛起等待鎖和自旋鎖
掛起等待鎖是重量級鎖的典型實現,是操作系統內核級別的,加鎖的時候發現競爭,就會使線程進入阻塞狀態,后續就需要內核進行喚醒了。競爭激烈,獲取鎖的周期更長,很難及時獲取到鎖,此時這個過程就不會消耗cpu資源,讓cpu去做其他的事情。
自旋鎖是輕量級鎖的典型實現,是應用程序級別的,加鎖的時候發現競爭,一般是不進入阻塞,而是通過忙等的形式進行等待。鎖競爭不激烈,獲取鎖的周期更短,等待鎖的過程中會一直消耗cpu資源。
4.普通互斥鎖和讀寫鎖
synchronized就是普通互斥鎖,只有加鎖和解鎖。而讀寫鎖存在讀方式加鎖,寫方式加鎖和解鎖。多個線程讀取一個數據本身就是線程安全的,但是當遇到多個線程讀數據,而有一個線程寫數據,那么就會涉及到線程安全問題了。當大部分操作在讀,少部分操作在寫,這時把鎖設置成普通互斥鎖就意味著鎖沖突會非常嚴重。讀寫鎖能確保讀鎖和讀鎖之間不互斥,而讀鎖和寫鎖,寫鎖和寫鎖之間產生互斥,在保證線程安全的前提下,減少鎖沖突的概率,提高效率。
Java標準庫中可以使用讀寫鎖,使用的是經典的lock和unlock寫法
5.可重入鎖和不可重入鎖
當一個線程針對一把鎖連續加鎖的時候,可重入鎖不會造成死鎖,synchronized就是可重入鎖。可重入鎖的要點有:1.鎖要記錄當前是哪個線程拿到鎖,2.統計加鎖的次數,在合適的時機釋放鎖。
6.公平鎖和非公平鎖
當多個線程爭取一把鎖的時候,鎖被釋放時,如果遵守先來后到的原則,那么這個鎖就是公平鎖。synchronized是非公平鎖,鎖在默認情況下被獲取到的概率是均等的,因為操作系統的調度是隨機的。要想實現公平鎖需要付出額外的代價,比如用一個隊列來記錄各個線程獲取鎖的順序。
7.synchronized
synchronized是自適應的鎖,不是讀寫鎖,是可重入鎖和非公平鎖。自適應是指,jvm會統計鎖競爭的激烈程度,來決定鎖是掛起等待鎖還是自旋鎖。鎖自適應的過程存在鎖升級,由無鎖升為偏向鎖,再身為自旋鎖,最后升級為掛起等待鎖。偏向鎖就是指當synchronized的時候不是一上來就加鎖,而是加一個標記,如果這個鎖沒有被競爭,就不會真正的加鎖,在解鎖的時候把這個標記刪除即可。如果這個鎖被競爭了,那就會真正加鎖。這個標記是非常輕量的,比加鎖解鎖高效得多。當前jvm只提供了鎖升級,不存在鎖降級。
8.鎖消除
這是編譯器優化的一種體現,編譯器會判定當前這個代碼是否真的需要加鎖,如果確實不需要加鎖,就會自動把synchronized刪去。這個優化的策略是比較保守的,所以我們加鎖的時候還是要仔細辨別。
9.鎖粗化
這里引入一個新的概念,鎖的粒度。當加鎖和解鎖之間包含的代碼越多(需要執行的指令),鎖的粒度就越粗,反之越細。一個代碼中反復針對細粒度的代碼進行加鎖,就可能優化成粗粒度的鎖。因為每次加鎖解鎖都會增加鎖的競爭,影響效率。
2.CAS
CAS是指比較和交換,compare and swap。
boolean CAS(address, expectedValue, swapValue) {if(&address == expectedValue) {address == swapValue;return true;}return false;
}
這里的偽代碼是指判定內存中的值是否和寄存器1的值一致,如果一致就把內存中的值和寄存器2的值進行交換。由于這里基本只關心內存里的值,而不關注寄存器2的值,所以可以把這里理解成賦值,基于交換實現的賦值。?
CAS是cpu的一條指令,所以它是原子的,這就對我們編寫多線程代碼產生了很大的作用。
CAS本質上是cpu的指令,操作系統把這個指令進行了封裝,提供了一些api,就可以在C++被調用了,而jvm又是C++實現的,所以jvm也能實現CAS操作。
1.原子類
CAS主要的應用是原子類。以下是Java標準庫中提供的原子類。原子類是一個專有名詞,特指atomic這個包里的這些類。
對boolean,int,long這些類型進行了封裝,確保性能,又能確保線程安全。?
以AtomicInteger里的getAndIncrement為例(以下是偽代碼)
class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;//可以把oldValue理解成寄存器while(CAS(oldValue, value, oldValue + 1) != true) {oldValue = value;}return oldValue;}
}
在計算的過程中,即使是多線程操作,因為CAS會不停對比寄存器和內存里的值,所以不會產生線程安全問題。?
2.基于CAS實現自旋鎖
public class SpinLock {private Thread owner;private void lock() {//通過CAS判斷當前鎖是否被線程持有//如果這個鎖已經被其他線程所持有,那么就自旋等待//如果這個鎖沒有被其他線程持有,那么鎖的擁有者就設為當前線程while(!CAS(this.owner, null, Thread.currentThread)) {}}private void unlock() {this.owner = null;}
}
3.CAS的典型缺陷?
CAS有一個典型的缺陷,是ABA問題。使用CAS能夠進行線程安全的編程的核心就是比較,比較內存和寄存器里的值。這里本質上就是在判定是否有其他線程插入進來進行了修改。我們認為如果內存和寄存器里的值一致,那么就沒有線程穿插進來修改。但實際上存在另一種情況,另一個線程把內存里的A改成B,又把B改回A。
ABA問題一般不會產生什么大問題,只有在極端情況下才會產生嚴重問題。
1.事例
以一個取錢的問題舉例,假設我的余額有1000,我在atm機前想取出500。由于我不當的操作,讓機器里產生了兩個線程來進行取錢操作。即使兩個線程穿插操作,因為CAS有判斷,所以不會產生什么問題。但如果在第一個線程的CAS執行完畢,余額變成500之后,剛好在那個瞬間有人往我的余額里轉了500,那么余額又變成了1000,此時第二個線程進行判斷后,就又會扣500。最終,我的余額就只有500了。
2.解決方法
在上述問題中,是使用余額來判定是否有其他線程插入,余額這個數值是既能增加又能減少,所以會產生ABA問題。如果這個時候我們引入一個其他指標,比如“版本號”,規定它只能增加,不能減少,每進行一次操作時,版本號就加1,就能有效避免ABA問題。
3.JUC相關的組件
這里的JUC指的是,java.util.concurrent這個包。這個包里封裝了和并發編程相關的東西。
1.Callable接口
這個接口和Runnable接口是并列關系。Runnable接口里面的run方法沒有返回值,Callable接口里面的call方法可以設置返回值
public Test {public static void main(String[] args) {Callable<Integer> callable = new Callable<>(){public int call() {int result = 0;for(int i = 0; i < 5000; i++) {result++;}return result;}};//Thread沒有提供參數是Callable的構造方法,所以要借助FutureTask這個類//注意這里的泛型類要和Callable的一致FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t = new Thread(futureTask);t.start();//get操作就是獲取FutureTask的返回值,這個返回值來自Callable里的call方法//get是可能會阻塞的,如果線程沒有執行完,get拿不到返回結果,那么它就會一直阻塞System.out.println(futureTask.get());}
}
2.ReentrantLock
ReentrantLock和synchronized是并列的,都是用來加鎖的。
1.synchronized是關鍵字(內部是通過C++實現的),ReentrantLock是類(內部是Java代碼實現的)。?
2.synchronized是通過代碼塊來加鎖,ReentrantLock是通過lock和unlock來加鎖的,注意unlock容易掉,搭配finally使用
3.ReentrantLock除了提供lock方法外,還提供了一個tryLock方法,這個方法加鎖不會阻塞,會返回true或者false,由開發者根據判定結果決定后續的操作。
4.ReentrantLock還提供了公平鎖的實現,它默認是非公平鎖,在new的時候往括號里填個true就能獲得一把公平鎖。
5.ReentrantLock搭配的等待通知機制是Condition類,功能比wait/notify更強大
3.Semaphore?
Semaphore指的是信息量,能夠協調多個線程之間的資源分配。
信息量表示的是“可用資源的個數”,申請一個資源(P操作,acquire)時就會+1,釋放一個資源(V操作,release)時就會-1。當計數器為0時,繼續申請就會阻塞等待。
Semaphore semaphore = new Semaphore(需要的信號量);
Semaphore有一種特殊情況,當信號量為1時,取值要么是1要么是0,此時就相當于一把鎖,我們也可以通過信號量的設置來限制最多有幾個線程來執行任務。?
4.CountDownLatch
使用多線程編程時,經常把一個大任務拆分成多個子任務,并發執行這些子任務,從而提高程序的效率。那我們要怎么衡量這些任務都完成了呢?這時就需要用到CountDownLatch。
1.基本使用
1.構造方法指定參數,描述一共有多少個任務
2.每個任務執行完畢之后,都調用countDown方法,當調用次數達到了設定的參數,則全部執行完
3.在主線程中調用await方法,等待所有任務執行完。
4.在多線程環境下使用集合類
我們在數據結構中學到的集合類大多都是線程不安全的
1.多線程下使用ArrayList
1.自行加鎖(推薦)
自己分析清楚哪些部分需要加鎖。
2.Collections synchronized(new ArrayList);
這個東西相當于套了一層殼,返回的所有List里的方法都是synchronized加鎖的。
3.使用CopyOnWrite
這個是編程中常見的一種思想方法,寫時拷貝。
假設我們有一個數組,現在有線程1對它進行修改,那么就會先復制一份這個數組,在復制的數組上進行修改,修改完之后在改變引用的指向。此時如果有其他線程來讀取這個數組,我們能保證要么這個線程讀到的時舊數組,或者是新數組,不會是修改到一半的數組。
缺陷
1.當這個數組非常大時,進行復制的開銷會很大。
2.當有多個線程進行修改操作時,也會產生很大的問題。
這個方法使用于特定的場景,比如服務器重新加載配置的時候。
2.多線程下使用HashMap
HashMap是線程不安全的,雖然HashTable是線程安全的,但是它是給所有方法都加鎖,效率不高。所以我們使用ConcurrentHashMap,它是按照桶級別進行加鎖,不是加了一個全局鎖,大幅降低了產生鎖沖突的概率。
上圖中的豎線標志對應的鏈表。
Concurrent的核心優化點
1.桶級別加鎖
當有多個線程進行修改時,如果修改的是不同鏈表上的值,本身就不涉及線程安全問題。如果在同一個鏈表上修改才會產生線程安全問題。?在實際開發中,使用的哈希表可能是非常大的,那么桶也會有很多,大概率是不會產生線程安全問題的。
2.size使用原子類
修改不同鏈表的過程不會產生線程安全問題,但是它們一起修改哈希表的size時,就會有問題了,這個時候我們就可以使用原子類來設置size
3.分段擴容
當我們想對哈希表進行擴容時,一次把所有的數據搬運完會比較耗時間,這是就可以分段搬運數據,進行多次put/get。