1.????前言~🥳🎉🎉🎉
Hello, Hello~ 親愛的朋友們👋👋,這里是E綿綿呀????。
如果你喜歡這篇文章,請別吝嗇你的點贊????和收藏📖📖。如果你對我的內容感興趣,記得關注我👀👀以便不錯過每一篇精彩。
當然,如果在閱讀中發現任何問題或疑問,我非常歡迎你在評論區留言指正🗨?🗨?。讓我們共同努力,一起進步!
加油,一起CHIN UP!💪💪
🔗個人主頁:E綿綿的博客
📚所屬專欄:1.?JAVA知識點專欄
? ? ?? ?深入探索JAVA的核心概念與技術細節
2.JAVA題目練習
? ? ? ??實戰演練,鞏固JAVA編程技能
3.c語言知識點專欄
? ? ? ? 揭示c語言的底層邏輯與高級特性
4.c語言題目練習
? ? ? ? 挑戰自我,提升c語言編程能力
5.Mysql數據庫專欄
? ? ? ? 了解Mysql知識點,提升數據庫管理能力
6.html5知識點專欄
? ? ? ? 學習前端知識,更好的運用它
7.?css3知識點專欄
? ? ? ? 在學習html5的基礎上更加熟練運用前端
8.JavaScript專欄
? ? ? ? 在學習html5和css3的基礎上使我們的前端使用更高級、
9.JavaEE專欄
? ? ? ? 學習更高階的Java知識,讓你做出網站
📘 持續更新中,敬請期待????
進階知識我們只需要了解即可,有個認識,因為它工作中基本不用,但面試中可能考?
2.常見的鎖策略
這是對于鎖的分類。對于該知識我們只需了解即可。
因為我們不自己去實現鎖,只是單純使用鎖,所以無需重點理解,只要簡單知道。
樂觀鎖和悲觀鎖:
樂觀鎖 :場景中它不太會出現鎖沖突
悲觀鎖 :場景中它非常容易出現鎖沖突
重量級鎖和輕量級鎖:
重量級鎖:該鎖開銷比較大(鎖沖突比較多),一個悲觀鎖,通常是重量級鎖(不絕對)
輕量級鎖:加鎖開銷比較小(鎖沖突比較少),一個樂觀鎖通常是輕量級鎖(不絕對)
自旋鎖和掛起等待鎖:
自旋鎖是輕量級鎖的一種典型實現,在用戶態下通過自旋方式(whlie循環)達到加鎖。因為是用戶態,所以開銷較小掛起等待鎖,是一種重量級鎖的典型實現,通過內核態借助系統提供的鎖機制 當出現鎖沖突的時候 會牽扯到內核對于線程的調度 是沖突線程出現掛起(阻塞等待)。因為牽涉到內核,所以開銷較大。
讀寫鎖:
把讀操作和寫操作分別加鎖。
如果兩個線程 兩個線程都是讀加鎖 不會產生鎖競爭
如果兩個線程 一個寫加鎖 另一個線程寫加鎖 會產生鎖競爭
如果兩個線程 一個線程寫加鎖 另一個線程讀加鎖 會產生鎖競爭(因為兩個讀并發不會引發線程不安全,讀和寫并發以及兩個寫并發會引發線程安全,所以針對該情況就有如上加鎖規則)
公平鎖 和 非公平鎖
公平鎖:遵守"先來先到" B比C先來 當A釋放鎖之后B就能比C先得到鎖
非公平鎖:不遵守先來后到功能,在A釋放鎖后b和c重新競爭。
操作系統自帶的鎖默認都屬于非公平鎖
如果想實現公平鎖,就需要依賴額外的數據結構, 來記錄線程們的先后順序.
可重入鎖 和 不可重入鎖
如果一個線程對一把鎖加鎖兩次會出現死鎖就是不可重入鎖
不出現死鎖就是可重入鎖
3.synchronized原理?
結合上面的鎖策略, 我們就可以總結出,Synchronized 具有以下特性(只考慮 JDK 1.8):
1. 開始時是樂觀鎖, 如果鎖沖突頻繁, 就轉換為悲觀鎖.
2. 開始是輕量級鎖實現, 如果鎖被持有的時間較長, 就轉換成重量級鎖.
3. 實現輕量級鎖的時候用到自旋鎖策略,實現重量級鎖的時候用到掛起等待鎖
4. 是一種不公平鎖
5. 是一種可重入鎖
6. 不是讀寫鎖 ?
它的原理有三個要講:鎖升級,鎖消除,鎖粗化。?
鎖升級
代碼中寫了一個synchronized之后 這里可能會產生一系列的"自適應的過程"鎖升級:
無鎖->偏向鎖->輕量級鎖->重量級鎖
偏向鎖: 不是真的鎖,并沒有加鎖,只是做了一個標記 如果有別的鎖來競爭了就會真正加鎖升級為輕量級鎖,如果沒有別的鎖競爭就不會真的加鎖。
加鎖本身有一定的開銷,能不加就不加,有競爭才加,這是懶漢模式的一個很好體現。
對于加鎖成為輕量級鎖后,如果競爭這把鎖的線程越來越多了(鎖沖突更激烈了),就從輕量級鎖升級成重量級鎖
鎖消除
編譯器,會智能的判定當前這個代碼是否有必要加鎖,如果你寫了加鎖,但是實際上沒有必要加鎖,編譯器就會把加鎖操作自動刪除掉。它保證優化之后的邏輯和之前的邏輯一致。
比如,在單個線程中,使用StringBuffer,編譯器就會進行優化消除掉鎖,且不影響邏輯。編譯器的鎖消除是非常小心的,如果沒有確定十分安全它是不會優化的。
鎖粗化
首先要學習鎖粗化就要講下關于鎖的粒度
如果加鎖操作里包含的實際要執行的代碼越多 就認為鎖的粒度越大所以如果有很多粒度較小的鎖,就可能短時間內出現頻繁創建銷毀鎖的情況,開銷就很大。編譯器就會通過合并多個鎖操作,減少鎖的開銷。這就是鎖粗化。
總結一下,鎖粗化是一種編譯器或運行時環境的優化技術,主要用于減少多線程程序中鎖操作的開銷。它的核心思想是將多個連續的鎖操作合并為一個更大的鎖操作,從而減少鎖的獲取和釋放次數,提高程序性能。
4.CAS
CAS的概念?
CAS是一種用于實現多線程同步的原子操作。它是現代并發編程中非常重要的底層機制,廣泛應用于無鎖算法、線程安全數據結構和并發控制中。?
這是CAS偽代碼
? boolean compareAndSwap(V, A, B) {if (&V == A) {&V = B;return true;}return false; }//這是CAS偽代碼,講述邏輯,不能實現?
CAS 操作包含三個操作數:
內存位置(V):需要更新的變量的內存地址。
期望值(A):變量的當前值。
新值(B):希望將變量更新為的值。
CAS 的操作邏輯是:
如果內存位置?
V
?的值等于期望值?A
,則將?V
?的值更新為新值?B,
返回true。如果?
V
?的值不等于?A
,則不做任何操作,返回false。
CAS 的特點?
原子性:
CAS 操作是硬件級別的原子操作,通常由 CPU 提供支持,然后封裝為api在java中使用。無鎖:
CAS 是一種無鎖編程技術,不需要使用傳統的鎖機制就能解決線程安全問題,因此可以避免鎖帶來的開銷。
CAS的應用
實現原子類?
要實現原子類,標準庫中提供了 java.util.concurrent.atomic 包, 里面的類都是基于內部有CAS從而實現了原子性 :這些類的方法使用時都不會被拆分為幾個指令,只能當作一個整體(一個指令)來看,所以無需鎖就能實現我們要的線程安全效果。
典型的就是 AtomicInteger 類. 其中的 getAndIncrement 相當于 i++ 操作.
public class Demo01_CAS {public static void main(String[] args) throws InterruptedException {//原子整型AtomicInteger atomicInteger = new AtomicInteger();Thread thread = new Thread(() -> {//五萬次自增操作for (int i = 0; i < 50000; i++) {atomicInteger.getAndIncrement();}});Thread thread2 = new Thread(() -> {//五萬次自增操作for (int i = 0; i < 50000; i++) {atomicInteger.getAndIncrement();}});//啟動線程thread.start();thread2.start();//等待兩個線程執行完成thread.join();thread2.join();System.out.println(atomicInteger);} }
最后結果為1w,就可以證明atomicinteger是個原子類,里面的方法都具有原子性,不會被拆分,不用鎖就能實現一樣的效果,且開銷更小。
實現自旋鎖?
CAS 可以用于實現自旋鎖,即線程在獲取鎖時不斷重試,而不是進入阻塞狀態。
public class SpinLock {private AtomicBoolean locked = new AtomicBoolean(false);public void lock() {while (!locked.compareAndSet(false, true)) {// 自旋等待}}public void unlock() {locked.set(false);} }
對于實現自旋鎖了解一下就行,而原子類一般用的比較多,要熟記?
CAS的ABA問題?
ABA 問題是 CAS 的一個經典問題。例如:
線程 1 讀取變量?
V
?的值為?A
。線程 2 將?
V
?的值從?A
?改為?B
,然后又改回?A
。線程 1 執行 CAS 操作,發現?
V
?的值仍然是?A
,誤認為V沒有發生變化。通常情況下ABA問題是不會發生bug的,但是特殊情況還是會導致一些問題。
比如我的賬戶里面有2000塊錢(狀態A),我委托張三說:如果我忘給李四轉1000塊錢,下午幫我轉一下,我在中午給李四轉了1000塊錢(狀態B),但是隨后公司發獎金1000到我的賬戶,此時我賬戶有1000塊錢(狀態A),張三下午檢查我賬戶,發現我有2000塊錢,于是又給李四轉了1000塊錢,此時就出現問題了,李四收到了兩次1000元,不符合我們的需求了.
那么解決方案是什么呢?
給預期值加一個版本號.在做CAS操作時,同時要更新預期值的版本號,版本號只增不減,每次操作都加一,在進行CAS比較的時候,不僅預期值要相同,版本號也要相同,這個時候才會返回true.
5.JUC(java.util.concurrent) 的常見類?
concurrent有并發的意思,所以這里的類都是并發編程相關的類?
Callable接口
Callable
?接口是 Java 并發編程中的一個重要接口,用于表示一個可以返回結果的任務。它與?Runnable
?接口類似,但?Callable
?更強大,因為它可以返回結果,而runable返回不了結果。
下面給一個代碼示例去教你怎么使用:
代碼示例: 創建線程計算 1 + 2 + 3 + ... + 1000, 使用 Callable 版本
1.創建一個匿名內部類, 實現 Callable 接口. Callable 帶有泛型參數. 泛型參數表示返回值的類型.
2.重寫 Callable 的 call 方法, 完成累加的過程. 直接通過返回值返回計算結果.
3.把 callable 實例使用 FutureTask 包裝一下.
4.創建線程, 線程的構造方法傳入 FutureTask . 此時新線程就會執行 FutureTask 內部的 Callable 的 call 方法, 完成計算. 計算結果就放到了 FutureTask 對象中.
5.在主線程中調用 futureTask.get() 能夠阻塞等待新線程計算完畢. 并獲取到 FutureTask 中的線程返回結果.
?
public class Demo {public static void main(String[] args) throws ExecutionException, InterruptedException {Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 1; i < 101; i++) {sum += i;TimeUnit.SECONDS.sleep(1);}return sum;}};//通過FutureTask來創建一個對象,這個對象持有CallableFutureTask<Integer> futureTask = new FutureTask<>(callable);//讓線程執行好定義的任務Thread thread = new Thread(futureTask);thread.start();Integer result = futureTask.get();System.out.println("最終的結果為:" + result);}
}?
?ReentrantLock 可重入鎖
和synchronized鎖類似。這個鎖提供了兩個方法:lock(上鎖) unlock(解鎖)
使用這個鎖的時候要注意解鎖,上鎖后,在代碼執行過程中,遇到return 或者異常終止了,就可能引起 unlock沒有被執行,鎖沒有釋放,其他地方想使用該鎖就會死等,因此,正確使用ReentrantLock鎖 是把unlock放在finall代碼塊中,這樣無論如何都能防止鎖未被釋放了。
import java.util.concurrent.locks.ReentrantLock;public class TryLockExample {private final ReentrantLock lock = new ReentrantLock();public void performTask() {if (lock.tryLock()) { // 嘗試獲取鎖try {System.out.println(Thread.currentThread().getName() + " 獲取鎖,正在執行任務");Thread.sleep(1000); // 模擬耗時操作} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock(); // 釋放鎖System.out.println(Thread.currentThread().getName() + " 釋放鎖");}} else {System.out.println(Thread.currentThread().getName() + " 未能獲取鎖,執行其他邏輯");}}public static void main(String[] args) {TryLockExample example = new TryLockExample();Runnable task = example::performTask;Thread thread1 = new Thread(task, "線程1");Thread thread2 = new Thread(task, "線程2");thread1.start();thread2.start();}
}
對于該鎖還有個trylock方法。
boolean tryLock()
:
嘗試獲取鎖,如果鎖可用則立即獲取并返回?
true;
否則返回?false且放棄獲取鎖并執行后續代碼
boolean tryLock(long timeout, TimeUnit unit)
:
在指定的時間內嘗試獲取鎖。
如果鎖在時間段內可獲取到,則獲取鎖并返回?
true
。如果超時時間到達仍未獲取鎖,則放棄鎖,返回?
false
。
優勢:
1.ReentrantLock,在加鎖的時候,有兩種方式. lock, tryLock. 給了咱們更多的可操作空間。
2.ReentrantLock,提供了公平鎖 的實現.(默認情況下是非公平鎖)3. ReentrantLock 提供了更強大的等待通知機制,搭配了 Condition 類實現等待通知(類似于wait,notify)
雖然 ReentrantLock 有上述優勢,但是咱們在加鎖的時候,還是首選synchronized。因為ReentrantLock 使用更加復雜,尤其是容易忘記解鎖,而上述優勢不算剛需,另外 synchronized 背后還有一系列的優化手段~~?
?semaphore 信號量
信號量,?來表?"可?資源的個數".本質上就是?個計數器.
就類似停車場:用N記錄當前可停車位,
有車進來停車,N-1;有車開走,N+1。這個N就表示可用資源的個數。
設“可?資源的個數"用 N來表示:
申請一個資源,會使N-1,稱為“P操作”;釋放一個資源,會使N+1,稱為“V操作”。如果N為0了,繼續P操作,該線程則會進行阻塞。
信號量是操作系統內部提供的一種機制,操作系統對應的api被JVM封裝下,就能通過java代碼來調用其相關的操作了。
在java中,用 acquire方法,表示申請;release方法,表示釋放。這兩個操作都是具有原子性的,可以直接使用。
對于鎖就是一種特殊的信號量,可以認為是計數值為1的信號量:釋放鎖狀態,就是1;加鎖狀態,就是0。對于這種非0即1的信號量,稱為二元信號量。
import java.util.concurrent.Semaphore;public class SemaphoreExample {public static void main(String[] args) {// 創建一個信號量,初始許可數為 4Semaphore semaphore = new Semaphore(4);// 創建一個任務Runnable runnable = new Runnable() {@Overridepublic void run() {try {System.out.println(Thread.currentThread().getName() + " 申請資源");semaphore.acquire(); // 獲取許可System.out.println(Thread.currentThread().getName() + " 獲取到資源了");Thread.sleep(1000); // 模擬資源占用System.out.println(Thread.currentThread().getName() + " 釋放資源了");semaphore.release(); // 釋放許可} catch (InterruptedException e) {e.printStackTrace();}}};// 創建多個線程并發執行任務for (int i = 1; i <= 6; i++) {new Thread(runnable, "線程" + i).start();}}
}
由于線程調度的不確定性,每次運行的結果可能略有不同,但整體邏輯是一致的。以下是可能的輸出:
注意一個線程可以多次acquire資源?
CountDownLatch?
CountDownLatch
?是 Java 并發包 (java.util.concurrent
) 中的一個同步工具,用于讓一個或多個線程等待其他線程完成操作。它的核心思想是通過一個計數器來實現線程的等待和通知機制。計數器初始化為一個正整數,每當一個線程完成任務時,計數器減 1;當計數器減到 0 時,等待的線程會被喚醒
CountDownLatch
?的核心方法:?
CountDownLatch(int count)
:
構造函數,初始化計數器。
void await()
:
使當前線程等待,直到計數器減到 0。
boolean await(long timeout, TimeUnit unit)
:
使當前線程等待,直到計數器減到 0 或超時。
void countDown()
:
將計數器減 1。如果計數器減到 0,則喚醒所有等待的線程。
long getCount()
:
返回當前計數器的值。
import java.util.concurrent.CountDownLatch;public class CountDownLatchExample {public static void main(String[] args) throws InterruptedException {int threadCount = 3;CountDownLatch latch = new CountDownLatch(threadCount); // 初始化計數器Runnable task = () -> {try {System.out.println(Thread.currentThread().getName() + " 開始執行任務");Thread.sleep(1000); // 模擬任務執行System.out.println(Thread.currentThread().getName() + " 完成任務");} catch (InterruptedException e) {e.printStackTrace();} finally {latch.countDown(); // 計數器減 1}};// 創建多個線程并發執行任務for (int i = 1; i <= threadCount; i++) {new Thread(task, "線程" + i).start();}System.out.println("主線程等待子線程完成任務");latch.await(); // 主線程等待計數器減到 0System.out.println("所有子線程完成任務,主線程繼續執行");}
}
6.線程安全的集合類
Vector,Stack,HashTable,是線程安全的 (但是Vector和hashTable不建議?,效率太低,只是方法加了synchronized) , 其他的集合類不是線程安全的.
?在多線程環境中使ArrayList線程安全的方式
1.用synchronized加鎖,保證線程安全.
2.使用Collectios.synchronizedList(new ArrayList)
synchronizedList可以讓ArrayList對象的關鍵操作都帶上synchronized,這樣就不用一個一個去填寫synchronized,一鍵速成
3.使用CopyOnWriteArrayList??
它是JUC包下的一個類,使用的是一種叫寫時復制技術來實現的
當要修改一個集合時,先復制這個集合的復本,然后修改復本的數據,修改完成后,用復本覆蓋原始集合,而讀集合時,則是讀取原版,所以對于多線程同時進行讀取和修改時,不用加鎖也不會引發bug(注意同時進行的操作中 修改最多只能有一個,不然有bug)
優點:在多讀少寫的情況下,無需加鎖就解決了ArrayList的線程安全問題,提高了性能。
缺點:對數組的修改不能太頻繁;數組不能太長,這些可能會導致復制操作成本太高。
?
多線程下使用哈希表
?HashMap是線程不安全的,HashTable是線程安全的
?多線程環境使用哈希表可以使用:
1.HashTable
2.ConcurrentHashMap
HashTable是把關鍵方法上都加了synchronized鎖,也就是synchronized給一整個哈希表加了鎖。當一個線程對數組中某條鏈表操作時,任何線程都不能對該數組操作,即使兩個線程操作的是不同的鏈表,不加鎖也不會有bug,但還是會進行堵塞,所以HashTable在多線程下的執行效率是很慢的。
ConcurrentHashMap: 對HashTable進行了改進和優化:
1.優化了加鎖方式
縮小了鎖的粒度,不再將整個數組都加鎖,對每個鏈表都分配了一把鎖(將每個鏈表的頭節點對象設為鎖),只有當多個線程訪問同一個鏈表時,才會產生鎖沖突。這樣就降低了鎖沖突,提高了效率。
2.充分利用CAS原子操作特性
?如size屬性通過CAS來更新.避免出現重量級鎖的情況.
3.優化了擴容方式
HashTable通過計算負載因子,判斷是否需要擴容,達到要擴容的值,就直接擴容:創建新數組,將原來的數據全復制到新數組中。當數據量非常大時,擴容操作會進行的比較慢,表現出來的就是在運行的某一時刻比較慢,不具有穩定性。
ConcurrentHashMap對此進行了優化,通過“化整為零”方式進行擴容,不是一下將全部數據進行拷貝,而是進行分批拷貝
當需要擴容時,先創建一個新的數組,每次將一部分數據拷貝到新數組中,后續每個來操作ConcurrentHashMap的線程,都會參與搬家的過程.每個操作負責搬運??部 分元素.這個過程中新老哈希表都存在,擴容結束,刪除舊表;這樣就很穩定。
4.對讀操作不進行加鎖
ConcurrentHashMap很特殊,進行讀寫操作并發執行時并不會引發線程安全問題,所以就無需對讀操作進行加鎖,能進一步減少開銷。
?所以現在我們一般在多線程中都用currenthashmap去使用哈希表。