干貨推薦|Java并發(fā)編程核心概念一覽,面試必備!
本文由讀者 muggle 投稿,muggle 是一位具備極客精神的 90 后單身老實猿,目前擔任騰訊云計算研發(fā)工程師,muggle 對 Java 并發(fā)編程有著深入研究,本文較長,大伙認真讀完一定會有所收獲。muggle 個人博客地址是 http://muggle.javaboy.org。
并行相關概念
同步和異步
同步和異步通常來形容一次方法的調用。同步方法一旦開始,調用者必須等到方法結束才能執(zhí)行后續(xù)動作;異步方法則是在調用該方法后不必等到該方法執(zhí)行完就能執(zhí)行后面的代碼,該方法會在另一個線程異步執(zhí)行,異步方法總是伴隨著回調,通過回調來獲得異步方法的執(zhí)行結果。
并發(fā)和并行
很多人都將并發(fā)與并行混淆在一起,它們雖然都可以表示兩個或者多個任務一起執(zhí)行,但執(zhí)行過程上是有區(qū)別的。并發(fā)是多個任務交替執(zhí)行,多任務之間還是串行的;而并行是多個任務同時執(zhí)行,和并發(fā)有本質區(qū)別。
對計算機而言,如果系統(tǒng)內只有一個 CPU ,而使用多進程或者多線程執(zhí)行任務,那么這種情況下多線程或者多進程就是并發(fā)執(zhí)行,并行只可能出現(xiàn)在多核系統(tǒng)中。當然,對 Java 程序而言,我們不必去關心程序是并行還是并發(fā)。
臨界區(qū)
臨界區(qū)表示的是多個線程共享但同時只能有一個線程使用它的資源。在并行程序中臨界區(qū)資源是受保護的,必須確保同一時刻只有一個線程能使用它。
阻塞
如果一個線程占有了臨界區(qū)的資源,其他需要使用這個臨界區(qū)資源的線程必須在這個臨界區(qū)進行等待(線程被掛起),這種情況就是發(fā)生了阻塞(線程停滯不前)。
死鎖\饑餓\活鎖
死鎖就是多個線程需要其他線程的資源才能釋放它所擁有的資源,而其他線程釋放這個線程需要的資源必須先獲得這個線程所擁有的資源,這樣造成了矛盾無法解開;如圖1情形就是發(fā)生死鎖現(xiàn)象:

活鎖就是兩個線程互相謙讓資源,結果就是誰也拿不到資源導致活鎖;就好比過馬路,行人給車讓道,車又給行人讓道,結果就是車和行人都停在那不走。
饑餓就是,某個線程優(yōu)先級特別低老是拿不到資源,導致這個線程一直無法執(zhí)行。
并發(fā)級別
并發(fā)級別分為阻塞,無饑餓,無障礙,無鎖,無等待幾個級別;根據(jù)名字我們也能大概猜出這幾個級別對應的什么情形;阻塞,無饑餓和無鎖都好理解;我們說一下無障礙和無等待;
無障礙:無障礙級別默認各個線程不會發(fā)生沖突,不會互相搶占資源,一旦搶占資源就認為線程發(fā)生錯誤,進行回滾。
無等待:無等待是在無鎖上的進一步優(yōu)化,限制每個線程完成任務的步數(shù)。
并行的兩個定理
加速比:加速比=優(yōu)化前系統(tǒng)耗時/優(yōu)化后系統(tǒng)耗時
Amdahl 定理:加速比=1/[F+(1-F)/n] 其中 n 表示處理器個數(shù) ,F(xiàn)是程序中只能串行執(zhí)行的比例(串行率);由公式可知,想要以最小投入,得到最高加速比即 F+(1-F)/n 取到最小值,F(xiàn) 和 n 都對結果有很大影響,在深入研究就是數(shù)學問題了;
Gustafson 定律:加速比=n-F(n-1),這兩定律區(qū)別不大,都體現(xiàn)了單純的減少串行率,或者單純的加 CPU 都無法得到最優(yōu)解。
Java 中的并行基礎
原子性,可見性,有序性
原子性指的是一個操作是不可中斷的,要么成功要么失敗,不會被其他線程所干擾;比如 int=1 ,這一操作在 cpu 中分為好幾個指令,但對程序而言這幾個指令是一體的,只有可能執(zhí)行成功或者失敗,不可能發(fā)生只執(zhí)行了一半的操作;對不同 CPU 而言保證原子性的的實現(xiàn)方式各有不同,就英特爾 CPU 而言是使用一個 lock 指令來保證的。
可見性指某一線程改變某一共享變量,其他線程未必會馬上知道。
有序性指對一個操作而言指令是按一定順序執(zhí)行的,但編譯器為了提高程序執(zhí)行的速度,會重排程序指令;cpu在執(zhí)行指令的時候采用的是流水線的形式,上一個指令和下一個指令差一個工步。比如A指令分三個工步:
操作內存a; 操作內存b; 操作內存c;
現(xiàn)假設有個指令 B 操作流程和 A 一樣,那么先執(zhí)行指令 A 再執(zhí)行指令 B 時間全利用上了,中間沒有停頓等待;但如果有三個這樣的指令在流水線上執(zhí)行:a>b>c ,b>e>c ,c>e>a ;這樣的指令順序就會發(fā)生等待降低了 CPU 的效率,編譯器為了避免這種事情發(fā)生,會適當優(yōu)化指令的順序進行重排。
volatile關鍵字
volatile 關鍵字在 Java 中的作用是保證變量的可見性和防止指令重排。
線程的相關操作
創(chuàng)建線程有三種方法
繼承Thread類創(chuàng)建線程 實現(xiàn)Runnable接口創(chuàng)建線程 使用Callable和Future創(chuàng)建線程
終止線程的方法
終止線程可調用 stop() 方法,但這個方法是被廢棄不建議使用的,因為強制終止一個線程會引起數(shù)據(jù)的不一致問題。比如一個線程數(shù)據(jù)寫到一半被終止了,釋放了鎖,其他線程拿到鎖繼續(xù)寫數(shù)據(jù),結果導致數(shù)據(jù)發(fā)生了錯誤。終止線程比較好的方法是“讓程序自己終止”,比如定義一個標識符,當標識符為 true 的時候直讓程序走到終點,這樣就能達到“自己終止”的目的。
線程的中斷等待和通知
interrupt() 方法可以中斷當前程序,object.wait() 方法讓線程進入等待隊列,object.notify() 隨機喚醒等待隊列的一個線程, object.notifyAll() 喚醒等待隊列的所有線程。object.wait() 必須在 synchronzied ?語句中調用;執(zhí)行wait、notify 方法必須獲得對象的監(jiān)視器,執(zhí)行結束后釋放監(jiān)視器供其他線程獲取。
join
join() 方法功能是等待其他線程“加入”,可以理解為將某個線程并為自己的子線程,等子線程走完或者等子線程走規(guī)定的時間,主線程才往下走;join 的本質是調用調用線程對象的 wait 方法,當我們執(zhí)行 wait 或者 notify 方法不應該獲取線程對象的監(jiān)聽器,因為可能會影響到其他線程的 join。
yield
yield 是線程的“謙讓”機制,可以理解為當線程搶到 cpu 資源時,放棄這次資源重新?lián)屨迹瑈ield() 是 Thread 里的一個靜態(tài)方法。
線程組
如果一個多線程系統(tǒng)線程數(shù)量眾多而且分工明確,那么可以使用線程組來分類。
public?void?contextLoads()?{
????ThreadGroup?testGroup=new?ThreadGroup("testGroup");
????Thread?a?=?new?Thread(testGroup,?new?MyRunnable(),?"a");
????Thread?b?=?new?Thread(testGroup,?new?MyRunnable(),?"b");
????a.start();
????b.start();
????int?i?=?testGroup.activeCount();
}
class?MyRunnable?implements?Runnable{
????@Override
????public?void?run()?{
????????System.out.println("test");
????}
}
圖示代碼創(chuàng)建了一個 testGroup 線程組。
守護線程
守護線程是一種特殊線程,它類似 Java 中的異常系統(tǒng),主要是概念上的分類,與之對應的是用戶線程。它功能應該是在后臺完成一些系統(tǒng)性的服務;設置一個線程為守護線程應該在線程 start 之前 setDaemon()。
線程優(yōu)先級
Java 中線程可以有自己的優(yōu)先級,優(yōu)先級高的更有優(yōu)勢搶占資源;線程優(yōu)先級高的不一定能搶占到資源,只是一個概率問題,而對應優(yōu)先級低的線程可能會發(fā)生饑餓。
在 Java 中使用1到10表示線程的優(yōu)先級,使用setPriority()方法來進行設置,數(shù)字越大代表優(yōu)先級越高。
Java 線程鎖
以下分類是從多個同角度來劃分,而不是以某一標準來劃分,請注意:
阻塞鎖:當一個線程獲得鎖,其他線程就會被阻塞掛起,直到搶占到鎖才繼續(xù)執(zhí)行,這樣會導致 CPU 切換上下文,切換上下文對 CPU 而言是很耗費時間的。 非阻塞鎖:當一個線程獲得鎖,其他線程直接跳過鎖資源相關的代碼繼續(xù)執(zhí)行,就是非阻塞鎖。 自旋鎖:當一個線程獲得鎖,其他線程則在不停進行空循環(huán),直到搶到鎖,這樣做的好處是避免了上下文切換。 可重入鎖:也叫做遞歸鎖,當一個線程獲得該鎖后,可以多次進入該鎖所同步著的代碼塊。 互斥鎖:互斥鎖保證了某一時刻只能有一個線程占有該資源。 讀寫鎖:將代碼功能分為讀和寫,讀不互斥,寫互斥。 公平鎖/非公平鎖:公平鎖就是在等待隊列里排最前面的的先獲得鎖,非公平鎖就是誰搶到誰用。 重量級鎖/輕量級鎖/偏向鎖:使用操作系統(tǒng) “Mutex Lock” 功能來實現(xiàn)鎖機制的叫重量級鎖,因為這種鎖成本高;輕量級鎖是對重量級鎖的優(yōu)化,提高性能;偏向鎖是對輕量級鎖的優(yōu)化,在無多線程競爭的情況下盡量減少不必要的輕量級鎖執(zhí)行路徑。
synchronized
屬于阻塞鎖、互斥鎖、非公平鎖以及可重入鎖,在 JDK1.6 以前屬于重量級鎖,后來做了優(yōu)化。
用法:
指定加鎖對象 用于靜態(tài)代碼塊/方法 用于動態(tài)代碼塊/方法
示例:
public?static?synchronized?void?test1(){
????System.out.println("test");
}
public??synchronized?void?test2(){
????System.out.println("test");
}??
public?void?test3(){
????synchronized?(Main.class){
????????System.out.println("test");
????}
}
當鎖加在靜態(tài)代碼塊上或者靜態(tài)方法上或者為 synchronized (xxx.class){} 時,鎖作用于整個類,凡是屬于這個類的對象的相關都會被上鎖,當用于動態(tài)方法或者為或者為synchronized (object){}時鎖作用于對象;除此之外,synchronized可以保證線程的可見性和有序性。
Lock
Lock 是一個接口,其下有多個實現(xiàn)類。
方法說明:
lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。 tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲?。?,則返回false,這個方法還可以設置一個獲取鎖的等待時長,如果時間內獲取不到直接返回。 兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那么對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。 unLock()方法是用來釋放鎖。 newCondition():生成一個和線程綁定的Condition實例,利用該實例我們可以讓線程在合適的時候等待,在特定的時候繼續(xù)執(zhí)行,相當于得到這個線程的wait和notify方法。
ReentrantLock
ReentrantLock 重入鎖,是實現(xiàn) Lock 接口的一個類,它對公平鎖和非公平鎖都支持,在構造方法中傳入一個 boolean 值,true 時為公平鎖,false 時為非公平鎖。
Semaphore(信號量)
信號量是對鎖的擴展,鎖每次只允許一個線程訪問一個資源,而信號量卻可以指定多個線程訪問某個資源,信號量的構造函數(shù)為
public?Semaphore(int?permits)?{
???sync?=?new?NonfairSync(permits);
}
public?Semaphore(int?permits,?boolean?fair)?{
???sync?=?fair???new?FairSync(permits)?:?new?NonfairSync(permits);
}
第一個方法指定了可使用的線程數(shù),第二個方法的布爾值表示是否為公平鎖。
acquire() 方法嘗試獲得一個許可,如果獲取不到則等待;tryAcquire() 方法嘗試獲取一個許可,成功返回 true,失敗返回false,不會阻塞,tryAcquire(int i) 指定等待時間;release() 方法釋放一個許可。
ReadWriteLock
讀寫分離鎖, 讀寫分離鎖可以有效的減少鎖競爭,讀鎖是共享鎖,可以被多個線程同時獲取,寫鎖是互斥只能被一個線程占有,ReadWriteLock 是一個接口,其中 readLock() 獲得讀鎖,writeLock() 獲得寫鎖 其實現(xiàn)類 ReentrantReadWriteLock 是一個可重入得的讀寫鎖,它支持鎖的降級(在獲得寫鎖的情況下可以再持有讀鎖),不支持鎖的升級(在獲得讀鎖的情況下不能再獲得寫鎖);讀鎖和寫鎖也是互斥的,也就是一個資源要么被上了一個寫鎖,要么被上了多個讀鎖,不會發(fā)生這個資即被上寫鎖又被上讀鎖的情況。
cas
cas(比較替換):無鎖策略的一種實現(xiàn)方式,過程為獲取到變量舊值(每個線程都有一份變量值的副本),和變量目前的新值做比較,如果一樣證明變量沒被其他線程修改過,這個線程就可以更新這個變量,否則不能更新;通俗的說就是通過不加鎖的方式來修改共享資源并同時保證安全性。
使用cas的話對于屬性變量不能再用傳統(tǒng)的 int ,long 等;要使用原子類代替原先的數(shù)據(jù)類型操作,比如 AtomicBoolean,AtomicInteger,AtomicInteger 等。
并發(fā)下集合類
并發(fā)集合類主要有:
ConcurrentHashMap:支持多線程的分段哈希表,它通過將整個哈希表分成多段的方式減小鎖粒度。 ConcurrentSkipListMap:ConcurrentSkipListMap的底層是通過跳表來實現(xiàn)的。跳表是一個鏈表,但是通過使用“跳躍式”查找的方式使得插入、讀取數(shù)據(jù)時復雜度變成了O(logn)。 ConCurrentSkipListSet:參考 ConcurrentSkipListMap。 CopyOnWriteArrayList:是 ArrayList 的一個線程安全的變形,其中所有可變操作(添加、設置,等等)都是通過對基礎數(shù)組進行一次新的復制來實現(xiàn)的。 CopyOnWriteArraySet:參考 CopyOnWriteArrayList。 ConcurrentLinkedQueue:cas 實現(xiàn)的非阻塞并發(fā)隊列。
線程池
介紹
多線程的設計優(yōu)點是能很大限度的發(fā)揮多核處理器的計算能力,但是,若不控制好線程資源反而會拖累cpu,降低系統(tǒng)性能,這就涉及到了線程的回收復用等一系列問題;而且本身線程的創(chuàng)建和銷毀也很耗費資源,因此找到一個合適的方法來提高線程的復用就很必要了。
線程池就是解決這類問題的一個很好的方法:線程池中本身有很多個線程,當需要使用線程的時候拿一個線程出來,當用完則還回去,而不是每次都創(chuàng)建和銷毀。在 JDK 中提供了一套 Executor 線程池框架,幫助開發(fā)人員有效的進行線程控制。
Executor 使用
獲得線程池的方法:
newFixedThreadPool(int nThreads) :創(chuàng)建固定數(shù)目線程的線程池。 newCachedThreadPool:創(chuàng)建一個可緩存的線程池,調用execute將重用以前構造的線程(如果線程可用)。如果現(xiàn)有線程沒有可用的,則創(chuàng)建一個新線 程并添加到池中。 newSingleThreadExecutor:創(chuàng)建一個單線程化的 Executor。 newScheduledThreadPool:創(chuàng)建一個支持定時及周期性的任務執(zhí)行的線程池。
以上方法都是返回一個 ExecutorService 對象,executorService.execute() 傳入一個 Runnable 對象,可執(zhí)行一個線程任務。
下面看示例代碼
public?class?Test?implements?Runnable{
?int?i=0;
?public?Test(int?i){
??this.i=i;
?}
?public?void?run()?{
??System.out.println(Thread.currentThread().getName()+"====="+i);
?}
????public?static?void?main(String[]?args)?throws?InterruptedException?{
??ExecutorService?cachedThreadPool?=?Executors.newCachedThreadPool();
??for(int?i=0;i<10;i++){
???cachedThreadPool.execute(new?Test(i));
???Thread.sleep(1000);
??}
?}
}
線程池是一個龐大而復雜的體系,本文定位是基礎,不對其做更深入的研究,感興趣的小伙伴可以自行查資料進行學習。
ScheduledExecutorService
newScheduledThreadPool(int corePoolSize) 會返回一個ScheduledExecutorService 對象,可以根據(jù)時間對線程進行調度;其下有三個執(zhí)行線程任務的方法:schedule(),scheduleAtFixedRate() 以及 scheduleWithFixedDelay() 該線程池可解決定時任務的問題。
示例:
class?Test?implements?Runnable?{
????private?String?testStr;
????Test(String?testStr)?{
????????this.testStr?=?testStr;
????}
????@Override
????public?void?run()?{
????????System.out.println(testStr?+?"?>>>>?print");
????}
????public?static?void?main(String[]?args)?{
????????ScheduledExecutorService?service?=?Executors.newScheduledThreadPool(10);
????????long?wait?=?1;
????????long?period?=?1;
????????service.scheduleAtFixedRate(new?MyScheduledExecutor("job1"),?wait,?period,?TimeUnit.SECONDS);
????????service.scheduleWithFixedDelay(new?MyScheduledExecutor("job2"),?wait,?period,?TimeUnit.SECONDS);
????????scheduledExecutorService.schedule(new?MyScheduledExecutor("job3"),?wait,?TimeUnit.SECONDS);//延時waits?執(zhí)行
????}
}
job1的執(zhí)行方式是任務發(fā)起后間隔 wait 秒開始執(zhí)行,每隔 period 秒(注意:不包括上一個線程的執(zhí)行時間)執(zhí)行一次;
job2的執(zhí)行方式是任務發(fā)起后間隔 wait 秒開始執(zhí)行,等線程結束后隔 period 秒開始執(zhí)行下一個線程;
job3只執(zhí)行一次,延遲 wait 秒執(zhí)行;
ScheduledExecutorService 還可以配合 Callable 使用來回調獲得線程執(zhí)行結果,還可以取消隊列中的執(zhí)行任務等操作,這屬于比較復雜的用法,我們這里掌握基本的即可,到實際遇到相應的問題時我們在現(xiàn)學現(xiàn)用,節(jié)省學習成本。
鎖優(yōu)化
減小鎖持有時間
減小鎖的持有時間可有效的減少鎖的競爭。如果線程持有鎖的時間越長,那么鎖的競爭程度就會越激烈。因此,應盡可能減少線程對某個鎖的占有時間,進而減少線程間互斥的可能。
減少鎖持有時間的方法有:
進行條件判斷,只對必要的情況進行加鎖,而不是整個方法加鎖。 減少加鎖代碼的行數(shù),只對必要的步驟加鎖。
減小鎖粒度
減小鎖的范圍,減少鎖住的代碼行數(shù)可減少鎖范圍,減小共享資源的范圍也可減小鎖的范圍。減小鎖共享資源的范圍的方式比較常見的有分段鎖,比如 ConcurrentHashMap ,它將數(shù)據(jù)分為了多段,當需要 put 元素的時候,并不是對整個 hashmap 進行加鎖,而是先通過 hashcode 來知道他要放在那一個分段中,然后對這個分段進行加鎖,所以當多線程 put 的時候,只要不是放在一個分段中,就實現(xiàn)了真正的并行的插入。
鎖分離
鎖分離最常見的操作就是讀寫分離了,讀寫分離的操作參考 ReadWriteLock 章節(jié),而對讀寫分離進一步的延伸就是鎖分離了。為了提高線程的并行量,我們可以針對不同的功能采用不同的鎖,而不是統(tǒng)統(tǒng)用同一把鎖。比如說有一個同步方法未進行鎖分離之前,它只有一把鎖,任何線程來了,只有拿到鎖才有資格運行,進行鎖分離之后就不是這種情形了——來一個線程,先判斷一下它要干嘛,然后發(fā)一個對應的鎖給它,這樣就能一定程度上提高線程的并行數(shù)。
鎖粗化
一般為了保證多線程間的有效并發(fā),會要求每個線程持有鎖的時間盡量短,也就是說鎖住的代碼盡量少。但是如果如果對同一個鎖不停的進行請求、同步和釋放,其本身也會消耗系統(tǒng)寶貴的資源,反而不利于性能的優(yōu)化 。比如有三個步驟:a、b、c,a同步,b不同步,c同步;那么一個線程來時候會上鎖釋放鎖然后又上鎖釋放鎖。這樣反而可能會降低線程的執(zhí)行效率,這個時候我們將鎖粗化可能會更好——執(zhí)行 a 的時候上鎖,執(zhí)行完 c 再釋放鎖。
鎖擴展
分布式鎖
JDK 提供的鎖在單體項目中不會有什么問題,但是在集群項目中就會有問題了。在分布式模型下,數(shù)據(jù)只有一份(或有限制),此時需要利用鎖的技術控制某一時刻修改數(shù)據(jù)的進程數(shù)。JDK 鎖顯然無法滿足我們的需求,于是就有了分布式鎖。
分布式鎖的實現(xiàn)有三種方式:
基于數(shù)據(jù)庫實現(xiàn)分布式鎖 基于緩存(redis,memcached,tair)實現(xiàn)分布式鎖 基于 Zookeeper 實現(xiàn)分布式鎖
基于redis的分布式鎖比較使用普遍,在這里介紹其原理和使用:
redis 實現(xiàn)鎖的機制是 setnx 指令,setnx 是原子操作命令,鎖存在不能設置值,返回 0 ;鎖不存在,則設置鎖,返回 1 ,根據(jù)返回值來判斷上鎖是否成功。看到這里你可能想為啥不先 get 有沒有值,再 set 上鎖;首先我們要知道,redis 是單線程的,同一時刻只可能有一個線程操作內存,然后 setnx 是一個操作步驟(具有原子性),而 get 再 set 是兩個步驟(不具有原子性)。如果使用第二種可能會發(fā)生這種情況:客戶端 a get發(fā)現(xiàn)沒有鎖,這個時候被切換到客戶端b,b get也發(fā)現(xiàn)沒鎖,然后b set,這個時候又切換到a客戶端 a set;這種情況下,鎖完全沒起作用。所以,redis分布式鎖,原子性是關鍵。
對于 web 應用中 redis 客戶端用的比較多的是 lettuce,jedis,redisson。springboot 的 redis 的 start 包底層是 lettuce ,但對 redis 分布式鎖支持得最好的是 redisson(如果用 redisson 你就享受不到 redis 自動化配置的好處了);不過 springboot 的 redisTemplete 支持手寫 lua 腳本,我們可以通過手寫 lua 腳本來實現(xiàn) redis 鎖。
代碼示例:
public?boolean?lockByLua(String?key,?String?value,?Long?expiredTime){
????String?strExprie?=?String.valueOf(expiredTime);
????StringBuilder?sb?=?new?StringBuilder();
????sb.append("if?redis.call(\"setnx\",KEYS[1],ARGV[1])==1?");
????sb.append("then?");
????sb.append("????redis.call(\"pexpire\",KEYS[1],KEYS[2])?");
????sb.append("????return?1?");
????sb.append("else?");
????sb.append("????return?0?");
????sb.append("end?");
????String?script?=?sb.toString();
????RedisCallback?callback?=?(connection)?->?{
????????return?connection.eval(script.getBytes(),?ReturnType.BOOLEAN,?2,?key.getBytes(Charset.forName("UTF-8")),strExprie.getBytes(Charset.forName("UTF-8")),?value.getBytes(Charset.forName("UTF-8")));
????};
????Boolean?execute?=?stringRedisTemplate.execute(callback);
????return?execute;
}
關于lua腳本的語法我就不做介紹了。
在 github 上也有開源的 redis 鎖項目,比如 spring-boot-klock-starter 感興趣的小伙伴可以去試用一下。
數(shù)據(jù)庫鎖
對于存在多線程問題的項目,比如商品貨物的進銷存,訂單系統(tǒng)單據(jù)流轉這種,我們可以通過代碼上鎖來控制并發(fā),也可以使用數(shù)據(jù)庫鎖來控制并發(fā),數(shù)據(jù)庫鎖從機制上來說分樂觀鎖和悲觀鎖。
悲觀鎖:
悲觀鎖分為共享鎖(S鎖)和排他鎖(X鎖),MySQL 數(shù)據(jù)庫讀操作分為三種——快照讀,當前讀;快照讀就是普通的讀操作,如:
select?*from?table
當前讀就是對數(shù)據(jù)庫上悲觀鎖了;其中 select ... lock in share mode 屬于共享鎖,多個事務對于同一數(shù)據(jù)可以共享,但只能讀不能修改。而下面三種 SQL :
select?...for?update
update?...?set...
insert?into?...
屬于排他鎖,排他鎖就是不能與其他鎖并存,如一個事務獲取了一個數(shù)據(jù)行的排他鎖,其他事務就不能再獲取該行的其他鎖,包括共享鎖和排他鎖,但是獲取排他鎖的事務是可以對數(shù)據(jù)行讀取和修改,排他鎖是阻塞鎖。
樂觀鎖:
就是很樂觀,每次去拿數(shù)據(jù)的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù),如果有則更新失敗。一種實現(xiàn)方式為在數(shù)據(jù)庫表中加一個版本號字段 version ,任何 update 語句 where 后面都要跟上 version=?,并且每次 update 版本號都加 1。如果 a 線程要修改某條數(shù)據(jù),它需要先 select 快照讀獲得版本號,然后 update ,同時版本號加一。這樣就保證了在 a 線程修改某條數(shù)據(jù)的時候,確保其他線程沒有修改過這條數(shù)據(jù),一旦其他線程修改過,就會導致 a 線程版本號對不上而更新失敗(這其實是一個簡化版的mvcc)。
樂觀鎖適用于允許更新失敗的業(yè)務場景,悲觀鎖適用于確保更新操作被執(zhí)行的場景。
并發(fā)編程相關
善用 Java8 Stream 對于生產者消費者模式,條件判斷是使用 while 而不是 if 懶漢單例采用雙重檢查和鎖保證線程安全 善用 Future 模式 合理使用 ThreadLocal
Java 8 引入 lambda 表達式使在 Java 中使用函數(shù)式編程很方便。而 Java 8 中的 stream 對數(shù)據(jù)的處理能使線程執(zhí)行速度得以優(yōu)化。Future 模式是一種對異步線程的回調機制;現(xiàn)在 cpu 都是多核的,我們在處理一些較為費時的任務時可使用異步,在后臺開啟多個線程同時處理,等到異步線程處理完再通過 Future 回調拿到處理的結果。
ThreadLocal 的實例代表了一個線程局部的變量,每條線程都只能看到自己的值,并不會意識到其它的線程中也存在該變量(這里原理就不說了,網上資料很多),總之就是我們如果想在多線程的類里面使用線程安全的變量就用 ThreadLocal ,但是請一定要注意用完記得 remove ,不然會發(fā)生內存泄漏。
總結
隨著后端發(fā)展,現(xiàn)在單體項目越來越少,基本上都是集群和分布式,這樣也使得 JDK ?的鎖慢慢變得無用武之地。但是萬變不離其宗,雖然鎖的實現(xiàn)方式變了,但其機制是沒變的;無論是分布式鎖還是 JDK 鎖,其目的和處理方式都是一個機制,只是處理對象不一樣而已。
我們在平時編寫程序時對多線程最應該注意的就是線程優(yōu)化和鎖問題。我們腦中要對鎖機制有一套體系,而對線程的優(yōu)化經驗在于平時的積累和留心
—?【 THE END 】— 本公眾號全部博文已整理成一個目錄,請在公眾號里回復「m」獲取! 3T技術資源大放送!包括但不限于:Java、C/C++,Linux,Python,大數(shù)據(jù),人工智能等等。在公眾號內回復「1024」,即可免費獲取??!
