作者博客地址 https://muggle.。 并行相關(guān)概念同步和異步同步和異步通常來形容一次方法的調(diào)用。同步方法一旦開始,調(diào)用者必須等到方法結(jié)束才能執(zhí)行后續(xù)動作;異步方法則是在調(diào)用該方法后不必等到該方法執(zhí)行完就能執(zhí)行后面的代碼,該方法會在另一個線程異步執(zhí)行,異步方法總是伴隨著回調(diào),通過回調(diào)來獲得異步方法的執(zhí)行結(jié)果。 并發(fā)和并行很多人都將并發(fā)與并行混淆在一起,它們雖然都可以表示兩個或者多個任務(wù)一起執(zhí)行,但執(zhí)行過程上是有區(qū)別的。并發(fā)是多個任務(wù)交替執(zhí)行,多任務(wù)之間還是串行的;而并行是多個任務(wù)同時執(zhí)行,和并發(fā)有本質(zhì)區(qū)別。 對計算機而言,如果系統(tǒng)內(nèi)只有一個 CPU ,而使用多進程或者多線程執(zhí)行任務(wù),那么這種情況下多線程或者多進程就是并發(fā)執(zhí)行,并行只可能出現(xiàn)在多核系統(tǒng)中。當然,對 Java 程序而言,我們不必去關(guān)心程序是并行還是并發(fā)。 臨界區(qū)臨界區(qū)表示的是多個線程共享但同時只能有一個線程使用它的資源。在并行程序中臨界區(qū)資源是受保護的,必須確保同一時刻只有一個線程能使用它。 阻塞如果一個線程占有了臨界區(qū)的資源,其他需要使用這個臨界區(qū)資源的線程必須在這個臨界區(qū)進行等待(線程被掛起),這種情況就是發(fā)生了阻塞(線程停滯不前)。 死鎖\饑餓\活鎖死鎖就是多個線程需要其他線程的資源才能釋放它所擁有的資源,而其他線程釋放這個線程需要的資源必須先獲得這個線程所擁有的資源,這樣造成了矛盾無法解開;如圖1情形就是發(fā)生死鎖現(xiàn)象: 活鎖就是兩個線程互相謙讓資源,結(jié)果就是誰也拿不到資源導(dǎo)致活鎖;就好比過馬路,行人給車讓道,車又給行人讓道,結(jié)果就是車和行人都停在那不走。 饑餓就是,某個線程優(yōu)先級特別低老是拿不到資源,導(dǎo)致這個線程一直無法執(zhí)行。 并發(fā)級別并發(fā)級別分為阻塞,無饑餓,無障礙,無鎖,無等待幾個級別;根據(jù)名字我們也能大概猜出這幾個級別對應(yīng)的什么情形;阻塞,無饑餓和無鎖都好理解;我們說一下無障礙和無等待; 無障礙:無障礙級別默認各個線程不會發(fā)生沖突,不會互相搶占資源,一旦搶占資源就認為線程發(fā)生錯誤,進行回滾。 無等待:無等待是在無鎖上的進一步優(yōu)化,限制每個線程完成任務(wù)的步數(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 都對結(jié)果有很大影響,在深入研究就是數(shù)學(xué)問題了。 Gustafson 定律: 加速比=n-F(n-1),這兩定律區(qū)別不大,都體現(xiàn)了單純的減少串行率,或者單純的加 CPU 都無法得到最優(yōu)解。 Java 中的并行基礎(chǔ)原子性,可見性,有序性原子性指的是一個操作是不可中斷的,要么成功要么失敗,不會被其他線程所干擾;比如 可見性指某一線程改變某一共享變量,其他線程未必會馬上知道。 有序性指對一個操作而言指令是按一定順序執(zhí)行的,但編譯器為了提高程序執(zhí)行的速度,會重排程序指令;cpu在執(zhí)行指令的時候采用的是流水線的形式,上一個指令和下一個指令差一個工步。比如A指令分三個工步:
現(xiàn)假設(shè)有個指令 B 操作流程和 A 一樣,那么先執(zhí)行指令 A 再執(zhí)行指令 B 時間全利用上了,中間沒有停頓等待;但如果有三個這樣的指令在流水線上執(zhí)行: volatile關(guān)鍵字volatile 關(guān)鍵字在 Java 中的作用是保證變量的可見性和防止指令重排。 線程的相關(guān)操作創(chuàng)建線程有三種方法
終止線程的方法 終止線程可調(diào)用 stop() 方法,但這個方法是被廢棄不建議使用的,因為強制終止一個線程會引起數(shù)據(jù)的不一致問題。比如一個線程數(shù)據(jù)寫到一半被終止了,釋放了鎖,其他線程拿到鎖繼續(xù)寫數(shù)據(jù),結(jié)果導(dǎo)致數(shù)據(jù)發(fā)生了錯誤。終止線程比較好的方法是“讓程序自己終止”,比如定義一個標識符,當標識符為 true 的時候直讓程序走到終點,這樣就能達到“自己終止”的目的。 線程的中斷等待和通知 interrupt() 方法可以中斷當前程序,object.wait() 方法讓線程進入等待隊列,object.notify() 隨機喚醒等待隊列的一個線程, object.notifyAll() 喚醒等待隊列的所有線程。object.wait() 必須在 synchronzied 語句中調(diào)用;執(zhí)行wait、notify 方法必須獲得對象的監(jiān)視器,執(zhí)行結(jié)束后釋放監(jiān)視器供其他線程獲取。 join join() 方法功能是等待其他線程“加入”,可以理解為將某個線程并為自己的子線程,等子線程走完或者等子線程走規(guī)定的時間,主線程才往下走;join 的本質(zhì)是調(diào)用調(diào)用線程對象的 wait 方法,當我們執(zhí)行 wait 或者 notify 方法不應(yīng)該獲取線程對象的監(jiān)聽器,因為可能會影響到其他線程的 join。 yield yield 是線程的“謙讓”機制,可以理解為當線程搶到 cpu 資源時,放棄這次資源重新?lián)屨?,yield() 是 Thread 里的一個靜態(tài)方法。 線程組如果一個多線程系統(tǒng)線程數(shù)量眾多而且分工明確,那么可以使用線程組來分類。
圖示代碼創(chuàng)建了一個 守護線程守護線程是一種特殊線程,它類似 Java 中的異常系統(tǒng),主要是概念上的分類,與之對應(yīng)的是用戶線程。它功能應(yīng)該是在后臺完成一些系統(tǒng)性的服務(wù);設(shè)置一個線程為守護線程應(yīng)該在線程 start 之前 setDaemon()。 線程優(yōu)先級Java 中線程可以有自己的優(yōu)先級,優(yōu)先級高的更有優(yōu)勢搶占資源;線程優(yōu)先級高的不一定能搶占到資源,只是一個概率問題,而對應(yīng)優(yōu)先級低的線程可能會發(fā)生饑餓。 在 Java 中使用1到10表示線程的優(yōu)先級,使用setPriority()方法來進行設(shè)置,數(shù)字越大代表優(yōu)先級越高。 Java 線程鎖以下分類是從多個同角度來劃分,而不是以某一標準來劃分,請注意:
synchronized屬于阻塞鎖、互斥鎖、非公平鎖以及可重入鎖,在 JDK1.6 以前屬于重量級鎖,后來做了優(yōu)化。 用法:
示例:
當鎖加在靜態(tài)代碼塊上或者靜態(tài)方法上或者為 LockLock 是一個接口,其下有多個實現(xiàn)類。 方法說明:
ReentrantLockReentrantLock 重入鎖,是實現(xiàn) Lock 接口的一個類,它對公平鎖和非公平鎖都支持,在構(gòu)造方法中傳入一個 boolean 值,true 時為公平鎖,false 時為非公平鎖。 Semaphore(信號量)信號量是對鎖的擴展,鎖每次只允許一個線程訪問一個資源,而信號量卻可以指定多個線程訪問某個資源,信號量的構(gòu)造函數(shù)為
第一個方法指定了可使用的線程數(shù),第二個方法的布爾值表示是否為公平鎖。 acquire() 方法嘗試獲得一個許可,如果獲取不到則等待;tryAcquire() 方法嘗試獲取一個許可,成功返回 true,失敗返回false,不會阻塞,tryAcquire(int i) 指定等待時間;release() 方法釋放一個許可。 ReadWriteLock讀寫分離鎖, 讀寫分離鎖可以有效的減少鎖競爭,讀鎖是共享鎖,可以被多個線程同時獲取,寫鎖是互斥只能被一個線程占有,ReadWriteLock 是一個接口,其中 readLock() 獲得讀鎖,writeLock() 獲得寫鎖 其實現(xiàn)類 ReentrantReadWriteLock 是一個可重入得的讀寫鎖,它支持鎖的降級(在獲得寫鎖的情況下可以再持有讀鎖),不支持鎖的升級(在獲得讀鎖的情況下不能再獲得寫鎖);讀鎖和寫鎖也是互斥的,也就是一個資源要么被上了一個寫鎖,要么被上了多個讀鎖,不會發(fā)生這個資即被上寫鎖又被上讀鎖的情況。 cascas(比較替換):無鎖策略的一種實現(xiàn)方式,過程為獲取到變量舊值(每個線程都有一份變量值的副本),和變量目前的新值做比較,如果一樣證明變量沒被其他線程修改過,這個線程就可以更新這個變量,否則不能更新;通俗的說就是通過不加鎖的方式來修改共享資源并同時保證安全性。 使用cas的話對于屬性變量不能再用傳統(tǒng)的 int ,long 等;要使用原子類代替原先的數(shù)據(jù)類型操作,比如 AtomicBoolean,AtomicInteger,AtomicInteger 等。 并發(fā)下集合類并發(fā)集合類主要有:
線程池介紹多線程的設(shè)計優(yōu)點是能很大限度的發(fā)揮多核處理器的計算能力,但是,若不控制好線程資源反而會拖累cpu,降低系統(tǒng)性能,這就涉及到了線程的回收復(fù)用等一系列問題;而且本身線程的創(chuàng)建和銷毀也很耗費資源,因此找到一個合適的方法來提高線程的復(fù)用就很必要了。 線程池就是解決這類問題的一個很好的方法:線程池中本身有很多個線程,當需要使用線程的時候拿一個線程出來,當用完則還回去,而不是每次都創(chuàng)建和銷毀。在 JDK 中提供了一套 Executor 線程池框架,幫助開發(fā)人員有效的進行線程控制。 Executor 使用獲得線程池的方法:
以上方法都是返回一個 ExecutorService 對象,executorService.execute() 傳入一個 Runnable 對象,可執(zhí)行一個線程任務(wù)。 下面看示例代碼
線程池是一個龐大而復(fù)雜的體系,本文定位是基礎(chǔ),不對其做更深入的研究,感興趣的小伙伴可以自行查資料進行學(xué)習。 ScheduledExecutorServicenewScheduledThreadPool(int corePoolSize) 會返回一個ScheduledExecutorService 對象,可以根據(jù)時間對線程進行調(diào)度;其下有三個執(zhí)行線程任務(wù)的方法:schedule(),scheduleAtFixedRate() 以及 scheduleWithFixedDelay() 該線程池可解決定時任務(wù)的問題。 示例:
job1的執(zhí)行方式是任務(wù)發(fā)起后間隔 job2的執(zhí)行方式是任務(wù)發(fā)起后間隔 job3只執(zhí)行一次,延遲 ScheduledExecutorService 還可以配合 Callable 使用來回調(diào)獲得線程執(zhí)行結(jié)果,還可以取消隊列中的執(zhí)行任務(wù)等操作,這屬于比較復(fù)雜的用法,我們這里掌握基本的即可,到實際遇到相應(yīng)的問題時我們在現(xiàn)學(xué)現(xiàn)用,節(jié)省學(xué)習成本。 鎖優(yōu)化減小鎖持有時間減小鎖的持有時間可有效的減少鎖的競爭。如果線程持有鎖的時間越長,那么鎖的競爭程度就會越激烈。因此,應(yīng)盡可能減少線程對某個鎖的占有時間,進而減少線程間互斥的可能。 減少鎖持有時間的方法有:
減小鎖粒度減小鎖的范圍,減少鎖住的代碼行數(shù)可減少鎖范圍,減小共享資源的范圍也可減小鎖的范圍。減小鎖共享資源的范圍的方式比較常見的有分段鎖,比如 鎖分離鎖分離最常見的操作就是讀寫分離了,讀寫分離的操作參考 ReadWriteLock 章節(jié),而對讀寫分離進一步的延伸就是鎖分離了。為了提高線程的并行量,我們可以針對不同的功能采用不同的鎖,而不是統(tǒng)統(tǒng)用同一把鎖。比如說有一個同步方法未進行鎖分離之前,它只有一把鎖,任何線程來了,只有拿到鎖才有資格運行,進行鎖分離之后就不是這種情形了——來一個線程,先判斷一下它要干嘛,然后發(fā)一個對應(yīng)的鎖給它,這樣就能一定程度上提高線程的并行數(shù)。 鎖粗化一般為了保證多線程間的有效并發(fā),會要求每個線程持有鎖的時間盡量短,也就是說鎖住的代碼盡量少。但是如果如果對同一個鎖不停的進行請求、同步和釋放,其本身也會消耗系統(tǒng)寶貴的資源,反而不利于性能的優(yōu)化 。比如有三個步驟:a、b、c,a同步,b不同步,c同步;那么一個線程來時候會上鎖釋放鎖然后又上鎖釋放鎖。這樣反而可能會降低線程的執(zhí)行效率,這個時候我們將鎖粗化可能會更好——執(zhí)行 a 的時候上鎖,執(zhí)行完 c 再釋放鎖。 鎖擴展分布式鎖JDK 提供的鎖在單體項目中不會有什么問題,但是在集群項目中就會有問題了。在分布式模型下,數(shù)據(jù)只有一份(或有限制),此時需要利用鎖的技術(shù)控制某一時刻修改數(shù)據(jù)的進程數(shù)。JDK 鎖顯然無法滿足我們的需求,于是就有了分布式鎖。 分布式鎖的實現(xiàn)有三種方式:
基于redis的分布式鎖比較使用普遍,在這里介紹其原理和使用: redis 實現(xiàn)鎖的機制是 setnx 指令,setnx 是原子操作命令,鎖存在不能設(shè)置值,返回 0 ;鎖不存在,則設(shè)置鎖,返回 1 ,根據(jù)返回值來判斷上鎖是否成功??吹竭@里你可能想為啥不先 get 有沒有值,再 set 上鎖;首先我們要知道,redis 是單線程的,同一時刻只可能有一個線程操作內(nèi)存,然后 setnx 是一個操作步驟(具有原子性),而 get 再 set 是兩個步驟(不具有原子性)。如果使用第二種可能會發(fā)生這種情況:客戶端 a get發(fā)現(xiàn)沒有鎖,這個時候被切換到客戶端b,b get也發(fā)現(xiàn)沒鎖,然后b set,這個時候又切換到a客戶端 a set;這種情況下,鎖完全沒起作用。所以,redis分布式鎖,原子性是關(guān)鍵。 對于 web 應(yīng)用中 redis 客戶端用的比較多的是 lettuce,jedis,redisson。springboot 的 redis 的 start 包底層是 lettuce ,但對 redis 分布式鎖支持得最好的是 redisson(如果用 redisson 你就享受不到 redis 自動化配置的好處了);不過 springboot 的 redisTemplete 支持手寫 lua 腳本,我們可以通過手寫 lua 腳本來實現(xiàn) redis 鎖。 代碼示例:
關(guān)于lua腳本的語法我就不做介紹了。 在 github 上也有開源的 redis 鎖項目,比如 spring-boot-klock-starter 感興趣的小伙伴可以去試用一下。 數(shù)據(jù)庫鎖對于存在多線程問題的項目,比如商品貨物的進銷存,訂單系統(tǒng)單據(jù)流轉(zhuǎn)這種,我們可以通過代碼上鎖來控制并發(fā),也可以使用數(shù)據(jù)庫鎖來控制并發(fā),數(shù)據(jù)庫鎖從機制上來說分樂觀鎖和悲觀鎖。 悲觀鎖: 悲觀鎖分為共享鎖(S鎖)和排他鎖(X鎖),MySQL 數(shù)據(jù)庫讀操作分為三種——快照讀,當前讀;快照讀就是普通的讀操作,如:
當前讀就是對數(shù)據(jù)庫上悲觀鎖了;其中
屬于排他鎖,排他鎖就是不能與其他鎖并存,如一個事務(wù)獲取了一個數(shù)據(jù)行的排他鎖,其他事務(wù)就不能再獲取該行的其他鎖,包括共享鎖和排他鎖,但是獲取排他鎖的事務(wù)是可以對數(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ù),一旦其他線程修改過,就會導(dǎo)致 a 線程版本號對不上而更新失?。ㄟ@其實是一個簡化版的mvcc)。 樂觀鎖適用于允許更新失敗的業(yè)務(wù)場景,悲觀鎖適用于確保更新操作被執(zhí)行的場景。 并發(fā)編程相關(guān)
Java 8 引入 lambda 表達式使在 Java 中使用函數(shù)式編程很方便。而 Java 8 中的 stream 對數(shù)據(jù)的處理能使線程執(zhí)行速度得以優(yōu)化。Future 模式是一種對異步線程的回調(diào)機制;現(xiàn)在 cpu 都是多核的,我們在處理一些較為費時的任務(wù)時可使用異步,在后臺開啟多個線程同時處理,等到異步線程處理完再通過 Future 回調(diào)拿到處理的結(jié)果。 ThreadLocal 的實例代表了一個線程局部的變量,每條線程都只能看到自己的值,并不會意識到其它的線程中也存在該變量(這里原理就不說了,網(wǎng)上資料很多),總之就是我們?nèi)绻朐诙嗑€程的類里面使用線程安全的變量就用 ThreadLocal ,但是請一定要注意用完記得 remove ,不然會發(fā)生內(nèi)存泄漏。 總結(jié)隨著后端發(fā)展,現(xiàn)在單體項目越來越少,基本上都是集群和分布式,這樣也使得 JDK 的鎖慢慢變得無用武之地。但是萬變不離其宗,雖然鎖的實現(xiàn)方式變了,但其機制是沒變的;無論是分布式鎖還是 JDK 鎖,其目的和處理方式都是一個機制,只是處理對象不一樣而已。 我們在平時編寫程序時對多線程最應(yīng)該注意的就是線程優(yōu)化和鎖問題。我們腦中要對鎖機制有一套體系,而對線程的優(yōu)化經(jīng)驗在于平時的積累和留心。 |
|