JavaEE初階-多線程進階2

文章目錄

  • 前言
  • 一、CAS
    • 1.1 CAS的概念
    • 1.2 原子類
    • 1.3 CAS的ABA問題
  • 二、JUC中常用類
    • 2.1 Callable接口
    • 2.2 ReentrantLock(可重入)
    • 2.3 Semaphore信號量
    • 2.4 CountDownLatch類
    • 2.5 CopyOnWriteArrayList類
    • 2.6 ConcurrentHashMap


前言

對于多線程進階的部分,更多總結的就是面試常考,但是工作中開發中不常用到的知識。


一、CAS

1.1 CAS的概念

CAS就是compare and swap的首字母縮寫,意味著比較和交換,這樣的一條指令即可完成比較和交換這一套操作,也就是說這套操作是原子的。
我們可以將CAS的流程想象成一個方法。
在這里插入圖片描述
這里的交換其實思想上更偏向于賦值,因為一般更關注于內存地址address中的內容而不關心寄存器reg2中的內容,所以就可以近似說這里的操作就是將reg2的值賦給了address地址。
CAS一般就是cpu中的一條指令,所以操作系統為了使用它完成這樣的操作就需要去提供這樣的CAS的api。然后JVM又對這樣的api進行了封裝,使得我們在java中也能夠使用CAS操作了。但是實際上這樣的CAS操作被封裝到了“unsafe”包當中,就是提醒大家容易出錯,不鼓勵直接使用CAS。

1.2 原子類

Java當中也有一些類對CAS進行了進一步的封裝,就比如說原子類。
在這里插入圖片描述
如上圖的AtomicInteger就相當于對int進行了封裝,對于它的++或者–操作都是原子的,實例代碼如下:

package thread;import java.util.concurrent.atomic.AtomicInteger;public class Demo41 {public static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 50000; i++) {//count++ 這里的對count的修改都是原子的count.getAndIncrement();//++count//count.incrementAndGet();//--count//count.decrementAndGet();//count--//count.getAndDecrement();//count+=10;//count.getAndAdd(10);}});Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count.getAndIncrement();}});t.start();t1.start();t.join();t1.join();System.out.println(count);}
}

這里的多線程代碼就是經典的兩個線程兩個循環來計算count值,因為這里的count使用到了原子類的方法,所以加一操作是原子性的,自然不存在線程安全的問題,也能夠得到正確結果。
那么使用這種原子性操作的意義是什么呢?意義就在于效率,因為鎖是一個很重量級的操作,如果操作沒有原子性在多線程的情況下就要加鎖,但是我們可以使用CAS從而不去使用鎖,從而提高代碼效率。這一套基于CAS不加鎖實現線程安全代碼的方式,也被稱為“無鎖編程”。但是CAS這種方法也就僅僅適用于少數場景。

1.3 CAS的ABA問題

屬于CAS的一個重要注意事項,CAS的核心就是“比較-發現相等-交換”->發現相等即數據沒發生任何改變,但是相等不等于沒改變過。可能值經歷了一個從A到B再到A的過程,這種情況在極端環境下會產生問題。
在這里插入圖片描述

如上圖取款操作,假如我們要取500,情急之下,我們多按了兩次取款按鈕,此時產生了兩個線程來進行扣款操作,但是如果在此時別人給你轉了500,那么就會出現問題了。
在這里插入圖片描述
如圖左邊是t1線程,右邊是t2線程,t2線程完成扣款五百之后,此時t3線程給賬戶又轉了500,此時應該不成立的t1線程的判斷又成立了,導致又完成一次扣款。上述的過程就是典型的ABA問題所造成的bug,是非常極端的情況。
如何去避免這樣的問題呢?可以約定一個版本號,每次進行扣款或存款都更新版本號,如果版本號沒有改變數據就一定沒變過。
在這里插入圖片描述
通過版本號約束就可以避免這里的ABA問題,避免多次扣款。即使t3線程仍然給賬戶匯了500,但是此時版本號已經是2了,所以t1線程的版本號對不上,方法內部的扣款操作無法完成,所以即使有兩個線程去扣款,扣的款也只有500。

二、JUC中常用類

JUC是java.util.concurrent這個包的首字母,在這里介紹一下這個包當中的常用類。

2.1 Callable接口

我們都知道Runnable接口用來表示一個待執行的任務,Callable接口和Runnable也是相似的,他也是用來表示一個待執行的任務,但是Callable有返回值,表示這個線程執行結束要得到的結果是啥。

public class Demo42 {private static int count = 0;public static void main(String[] args) throws InterruptedException, ExecutionException {//使用Runnable來求出1~100的和Thread t = new Thread(new Runnable() {@Overridepublic void run() {int result = 0;for (int i = 1; i <= 100; i++) {result += i;}//需要用成員變量來接收值 主線程和t線程的耦合程度高 如果有多個這樣的線程就不方便了count = result;}});

以上給出了一段代碼,就是使用類變量count來得到線程結果,這樣的代碼等線程多了之后很不方便,代碼不夠優雅。Callable就是用來解決上述代碼的問題的。接下來給出全部代碼用于對比:

package thread;import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class Demo42 {private static int count = 0;public static void main(String[] args) throws InterruptedException, ExecutionException {//使用Runnable來求出1~100的和Thread t = new Thread(new Runnable() {@Overridepublic void run() {int result = 0;for (int i = 1; i <= 100; i++) {result += i;}//需要用成員變量來接收值 主線程和t線程的耦合程度高 如果有多個這樣的線程就不方便了count = result;}});// t.start();// t.join();// System.out.println(count);// Callable和Runnable很相似 但是Runnable可以返回計算的值Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int result = 0;for (int i = 1; i <= 100; i++) {result += i;}return result;}};// futuretask這個類用來包裝callable這個類 這樣callable就可以直接放入線程FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t2 = new Thread(futureTask);t2.start();//從future獲取線程啟動通過callable計算得到的值t2.join();System.out.println(futureTask.get());}}

如以上代碼,Callable接口需要使用FutureTask來包裝,包裝之后就將FutureTask對象放入線程,線程執行完成之后就可以通過FutureTask對象來得到線程執行的結果。

2.2 ReentrantLock(可重入)

在以前的JDK中,synchronized還沒現在那么好用,那時ReentrantLock還是非常有市場的。但是隨著版本的迭代,synchronized越來越強,基本上需要加鎖的時候無腦使用synchronized大概率不會出問題。那么ReentrantLock現在還有什么價值?
(1)ReentrantLock實現了公平鎖
在這里插入圖片描述
這里代碼中的參數寫true就是公平鎖,false就是非公平鎖。
(2)ReentrantLock提供了tryLock操作,給加鎖提供了更多的操作空間。
嘗試加鎖,如果該鎖已經被獲取了,那么就直接失敗返回,不會繼續等待。tryLock還有一個類似版本就是可以指定等待的時間,超時后返回。
(3)synchronized搭配wait以及notify的等待通知機制,ReentrantLock搭配Condition類完成等待通知。
Condition類比wait以及notify強一點。(多個線程wait,notify喚醒隨機一個。Condition指定線程喚醒)

2.3 Semaphore信號量

信號量是一個非常簡單的概念,就是一個計數器,描述了可用資源的數目。圍繞信號量有兩個操作,P操作,計數器減一,申請資源,V操作,計數器加一,釋放資源。提出信號量的是荷蘭人,PV是荷蘭語的首字母,在英語中是acquire就是獲取,以及release表示釋放。代碼示例如下:

package thread;import java.util.concurrent.Semaphore;public class Demo44 {public static void main(String[] args) throws InterruptedException {// 四個可用資源 P申請資源 V釋放資源Semaphore semaphore = new Semaphore(4);semaphore.acquire(1);System.out.println("P操作");semaphore.acquire(1);System.out.println("P操作");semaphore.acquire(1);System.out.println("P操作");semaphore.acquire(1);System.out.println("P操作");// 此時信號量的四個資源已經被申請完了// 如果繼續申請的話就會堵塞 因為要等別的線程釋放信號量的資源semaphore.acquire(1);}}

以上代碼信號量擁有四個單位的資源,然后通過acquire方法來申請資源,當資源被申請完并且沒有資源釋放時,再次申請資源就會阻塞。當設置信號量資源為一個單位,則信號量取值只能為1或者0,此時的信號量可以當成鎖來使用。代碼示例如下:

package thread;import java.util.concurrent.Semaphore;public class Demo45 {public static int count = 0;public static void main(String[] args) throws InterruptedException {//設置 1 0 信號量Semaphore semaphore = new Semaphore(1);Thread t1 = new Thread(() -> {try {for (int i = 0; i < 50000; i++) {semaphore.acquire(1);count++;semaphore.release();}} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t2 = new Thread(() -> {try {for (int i = 0; i < 50000; i++) {semaphore.acquire(1);count++;semaphore.release();}} catch (InterruptedException e) {throw new RuntimeException(e);}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

以上代碼其實還是多線程代碼的經典例子,使用兩個線程來計算累加值。當t1進行count加一的操作時,它已經申請了唯一的信號量資源,此時如果t2線程也想進行count加一就必須先執行申請信號量資源的操作,此時就會阻塞,只有當t1線程的count++執行結束之后釋放資源,t2線程才能繼續執行,這就實現了count++操作的原子性,從而避免線程安全問題。

2.4 CountDownLatch類

相對來說比較實用的工具類,當我們把一個任務分為多個時,就可以通過這個工具類來識別任務是否整體執行完畢了。代碼示例如下:

package thread;import java.util.concurrent.CountDownLatch;public class Demo46 {public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(10);for (int i = 0; i < 10; i++) {int temp = i;Thread t = new Thread(() -> {System.out.println("線程啟動:" + temp);//當作任務try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("線程結束:" + temp);latch.countDown();});t.start();}//等待所有線程中的任務結束latch.await();System.out.println("所有線程結束。");}}

這段代碼中我們給CountDownLatch類對象的參數為10,并且創建10個線程去執行任務,并且在每個線程中使用countDown方法,countDown方法相當于計數,當一個線程結束就會加一,latch.await()方法就會等待所有線程執行結束,當countDown方法累加的數等于初始化CountDownLatch對象的參數時await方法就會停止等待,整個代碼就運行結束了。這里代碼中CountdownLatch對象的參數和線程數相等,并且每個線程都放了countDown方法,所以所有線程運行結束await方法也就不等了。

2.5 CopyOnWriteArrayList類

ArrayList,LinkedList,Stack,Queue,HashMap…在多線程下使用集合類需要注意線程安全問題。Vector自帶了synchronized,Stack繼承自Vector所以也有synchronized,HashTable也是自帶synchronized。但是需要注意一點,加鎖不代表就是線程安全的,不加鎖也不能確定線程就是一定不安全的,需要具體代碼具體分析。
在我們使用未加鎖的類時需要手動進行加鎖,這樣是比較麻煩的,標準庫提供了一些其它的解決方案,如下圖。
在這里插入圖片描述
通過這樣的操作,給ArrayList這些集合類套一層殼,就是給一些關鍵方法加上了synchronized,使得ArrayList達到Vector那樣的效果。
CopyOnWriteArrayList類也是一種解決線程安全問題的方法。
在這里插入圖片描述
如果當前有多個線程讀列表上的數據,那么不需要做任何處理。如果某個線程對上面的數據進行修改,此時另一個線程進行讀取,那么很可能會讀到200 3這樣的中間情況。CopyOnWriteArrayList這樣的類就是一種寫時拷貝,在你對列表進行修改時會開辟新空間在新空間上進行修改,你要讀取數據那么就在舊空間進行讀取,當修改完成后將新的列表的引用代替舊的引用,舊的空間就可以釋放了。這樣的過程沒有任何的加鎖和阻塞,也能保證線程讀不到錯誤的數據。
這種方法的思想應用的很廣,例如顯卡渲染畫面到顯示器,顯示的動態效果其實就是很多張圖片,由于顯卡渲染足夠快這些圖片就能融合在一起,看到動畫效果。實際上就是寫時拷貝,在顯示上一個畫面的時候,在背后的額外空間生成下一個畫面,生成完畢了用下一個畫面代替上一個畫面。

2.6 ConcurrentHashMap

我們知道HashMap是線程不安全的,HashTable是帶鎖的,是否是線程安全的?事實上并不推薦使用這個,標準庫提供了更好的替代也就是ConcurrentHashMap。
HashTable加鎖就是簡單粗暴的給每個方法加了synchronized,就相當于針對this加鎖,只要針對HashTable上的元素進行操作,就都會涉及到鎖沖突。
ConcurrentHashMap做出了以下優化:
(1)使用鎖桶的方式來代替一把全局鎖,有效降低沖突概率。
在這里插入圖片描述
這一點很好理解,如果有兩個線程針對兩個不同的鏈表進行操作,那么它們之間是不會產生鎖沖突的。本身兩個線程修改的是不同的鏈表,也沒涉及到“公共變量”,所以不涉及線程安全問題。這個提升是非常大的,因為一個哈希表上的桶非常多,桶之間發生沖突的概率非常小,并且synchronized我們前面的博客也講過了,只要不發生沖突synchronized只是加了一個偏向鎖,就類似一個標記,消耗非常小。
(2)對于哈希表的size即使你修改的不同鏈表/桶,但是你在多線程的情況下也會涉及到多個線程修改一個公共變量的問題,在ConcurrentHashMap中對于size的修改就是使用CAS這種具有原子性的語句來完成,這樣不僅避免了加鎖這種重量級的操作,也解決了線程安全的問題。
(3)針對擴容進行了特殊優化。
如果發現負載因子太大了,那么就需要擴容,然而擴容又是比較低效的操作,普通的HashMap要在一次put的過程中完成整個擴容過程,就會使得put操作非常卡。ConcurrentHashMap就會在擴容的時候整出另外的一份空間,每次進行哈希表的基本操作都會將一部分擴容之前空間的數據搬到新空間,不是一口氣搬完而是分多次,在搬的過程中如果是插入操作就將新數據插入到新空間,刪除操作,新舊空間都進行刪除,查找操作,新舊空間都要查找。
另外值得一提的是,在java8之前ConcurrentHashMap是基于分段鎖的形式進行實現的,就是引入多個鎖對象,每個鎖對象去管理若干個哈希桶。相比于HashTable這個方法是進化,但是還是不如直接鎖桶,后面就把這個方法給廢棄了。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/12594.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/12594.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/12594.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Scala學習之 控制結構和函數

目錄 第二章 控制結構和函數1- 條件表達式2- 語句終止3- 塊表達式和賦值4- 輸入和輸出5- 循環6- 高級for循環和for推到式7- 函數8- 默認參數和帶名參數9- 可變參數10- 過程11- 懶值12- 異常end 第二章 控制結構和函數 1- 條件表達式 Scala的 if/esle 語法結構與java一樣, 但是…

C語言例題43、打印倒立金字塔

#include <stdio.h>void main() {int i, j;for (i 5; i > 0; i--) {for (j 5; j > i; j--) {//輸出空格printf(" ");}for (j 2 * i; j > 1; j--) {//輸出星號printf("* ");}printf("\n");} }運行結果&#xff1a; 本章C語言…

用好 explain 媽媽再也不用擔心我的 SQL 慢了

大家好&#xff0c;我是聰&#xff0c;一個樂于分享的小小程序員。在不久之前我寫了一個慢 SQL 分析工具&#xff0c;可以用來分析 Java Mybatis 項目的 SQL 執行情況&#xff0c;其中剛好涉及到了 explain 的使用。感興趣的可以了解一下。 Github 地址?&#xff1a;https://…

【C#】學習獲取程序執行路徑,Gemini 幫助分析

一、前言&#xff1a; 在Delphi中&#xff0c;如果想要獲取當前執行程序的目錄&#xff0c;程序代碼如下&#xff1a; ExtractFilePath(ParamStr(0)); 今天在分析一個別人做的C#程序時看到了一段C#代碼&#xff0c;意思是獲取執行程序所在的文件目錄&#xff1a; public stat…

基于區塊鏈的Web 3.0關鍵技術研討會順利召開

基于區塊鏈的Web3.0關鍵技術研討會 2024年4月23日&#xff0c;由國家區塊鏈技術創新中心主辦的“基于區塊鏈的web3.0關鍵技術研討會”召開。Web3.0被用來描述一個運行在“區塊鏈”技術之上的“去中心化”的互聯網&#xff0c;該網絡上的主體掌握自己數據所有權和使用權&#xf…

【回眸】git VS repo 區別

git VS repo 區別 1. git&#xff1a;Git是一個開源的分布式版本控制系統&#xff0c;用以有效、高速的處理從很小到非常大的項目版本管理。 2. Repo: Repo是谷歌用Python腳本寫的調用git的一個腳本,Repo實現管理多個git庫。 Git 常用命令 1. git init&#xff1a;在當前目…

【原創】java+springboot+mysql企業郵件管理系統設計與實現

個人主頁&#xff1a;程序猿小小楊 個人簡介&#xff1a;從事開發多年&#xff0c;Java、Php、Python、前端開發均有涉獵 博客內容&#xff1a;Java項目實戰、項目演示、技術分享 文末有作者名片&#xff0c;希望和大家一起共同進步&#xff0c;你只管努力&#xff0c;剩下的交…

Vue的學習 —— <vue組件>

目錄 前言 正文 一、選項式API與組合式API 二、生命周期函數 1、onBeforeMount() 2、onMounted() 3、onBeforeUpdate() 4、onUpdated() 5、onBeforeUnmount() 6、onUnmounted() 三、組件之間的樣式沖突 四、父組件向子組件傳遞數據 1、定義props 2、靜態綁定props…

C++青少年簡明教程:賦值語句

C青少年簡明教程&#xff1a;賦值語句 賦值語句是編程中最基本也是最常用的概念之一&#xff0c;它用于將一個值分配給一個變量。 使用等號&#xff08; 稱為賦值運算符&#xff09;來給變量賦值&#xff0c;賦值語句的左邊是要賦值的變量&#xff0c;右邊是要賦給變量的值。C…

Docker 使用 CentOS 鏡像

使用 docker run 直接運行 CentOS 7 鏡像&#xff0c;并登錄 bash。 C:\Users\yhu>docker run -it centos:centos7 bash Unable to find image centos:centos7 locally centos7: Pulling from library/centos 2d473b07cdd5: Pull complete Digest: sha256:be65f488b7764ad36…

GPT-4o:全面深入了解 OpenAI 的 GPT-4o

GPT-4o&#xff1a;全面深入了解 OpenAI 的 GPT-4o 關于 GPT-4o 的所有信息ChatGPT 增強的用戶體驗改進的多語言和音頻功能GPT-4o 優于 Whisper-v3M3Exam 基準測試中的表現 GPT-4o 的起源追蹤語言模型的演變GPT 譜系&#xff1a;人工智能語言的開拓者多模式飛躍&#xff1a;超越…

連接虛擬機的 redis

用Windows 的 Redis Insight 連接虛擬機的 安裝redis發現連不上 我的redis是新安裝&#xff0c;沒有用戶名密碼&#xff0c;發現是ip問題 127 開頭的被我注釋了&#xff0c;換成了ifconfig查到的ip

vim命令大全(基礎版)

創建一個py文件 vim cs.py一、命令模式 按Esc后&#xff0c;按shift&#xff1a;進入命令模式 :wq # 保存并退出 :q # 退出 :q! # 強制退出 :%d # 刪除全部內容按兩下d&#xff0c;刪除光標所在行 按兩下y&#xff0c;復制光標所在行 按一下p&#xff0c;粘貼復制內容到下…

Android性能:SurfaceFlinger與BufferQueue(3)

Android性能&#xff1a;SurfaceFlinger與BufferQueue&#xff08;3&#xff09; Android顯示系統的組成可以概括為兩大部分&#xff1a;繪制(DrawFrame)合成&#xff08;SurfaceFlinger HWC&#xff09; 繪制&#xff1a;Surface中空的 GraphicBuffer->CPU或者GPU通過Canv…

Python GUI開發- Qt Designer環境搭建

前言 Qt Designer是PyQt5 程序UI界面的實現工具&#xff0c;使用 Qt Designer 可以拖拽、點擊完成GUI界面設計&#xff0c;并且設計完成的 .ui 程序可以轉換成 .py 文件供 python 程序調用 環境準備 使用pip安裝 pip install pyqt5-toolsQt Designer 環境搭建 在pip安裝包…

Vue 常見通信

Vue 常見通信 1、父子通信 父傳子 props&#xff0c;子傳父 events&#xff08;$emit&#xff09;&#xff1b; 通過父鏈 / 子鏈 通信$parent / $children&#xff1b; $refs獲取ref 可以訪問組件實例方法&#xff0c;&#xff1b; 提供與注射provide / inject a t t r s …

使用Processing和PixelFlow庫創建交互式流體太極動畫

使用Processing和PixelFlow庫創建交互式流體太極動畫 引言準備工作效果展示代碼結構代碼解析第一部分&#xff1a;導入庫和設置基本參數第二部分&#xff1a;流體類定義MyFluidDataConfig 類詳解MyFluidData 類詳解my_update 方法詳解流體類定義完整代碼 第三部分&#xff1a;太…

找數字-算法

解法一、數位模擬 比n大的最小數就是n1&#xff0c;當n1時&#xff0c;以下幾種情況會導致n中1的個數發生變化&#xff08;或者不變&#xff09; 1.n的低位連續1的個數count>1&#xff0c;如1011&#xff0c;10111,1111等&#xff0c;加1后使得n中1的個數減少count-1個 解…

基于SVPWM的飛輪控制系統的simulink建模與仿真

目錄 1.課題概述 2.系統仿真結果 3.核心程序與模型 4.系統原理簡介 5.完整工程文件 1.課題概述 基于SVPWM的飛輪控制系統的simulink建模與仿真。SVPWM的核心思想是將逆變器輸出的三相電壓矢量在兩相靜止坐標系&#xff08;αβ坐標系&#xff09;中表示&#xff0c;通過控…

Python3 數據類型詳解:掌握數據基石,編寫高效程序

Python3 中的基本數據類型包括整數&#xff08;int&#xff09;、浮點數&#xff08;float&#xff09;、布爾值&#xff08;bool&#xff09;、字符串&#xff08;str&#xff09;、列表&#xff08;list&#xff09;、元組&#xff08;tuple&#xff09;、集合&#xff08;se…