1. 干貨推薦|Java并發(fā)編程核心概念一覽,面試必備!

        共 8760字,需瀏覽 18分鐘

         ·

        2020-08-22 05:40

        本文由讀者 muggle 投稿,muggle 是一位具備極客精神的 90 后單身老實(shí)猿,目前擔(dān)任騰訊云計(jì)算研發(fā)工程師,muggle 對(duì) Java 并發(fā)編程有著深入研究,本文較長(zhǎng),大伙認(rèn)真讀完一定會(huì)有所收獲。muggle 個(gè)人博客地址是 http://muggle.javaboy.org。

        并行相關(guān)概念

        同步和異步

        同步和異步通常來形容一次方法的調(diào)用。同步方法一旦開始,調(diào)用者必須等到方法結(jié)束才能執(zhí)行后續(xù)動(dòng)作;異步方法則是在調(diào)用該方法后不必等到該方法執(zhí)行完就能執(zhí)行后面的代碼,該方法會(huì)在另一個(gè)線程異步執(zhí)行,異步方法總是伴隨著回調(diào),通過回調(diào)來獲得異步方法的執(zhí)行結(jié)果。

        并發(fā)和并行

        很多人都將并發(fā)與并行混淆在一起,它們雖然都可以表示兩個(gè)或者多個(gè)任務(wù)一起執(zhí)行,但執(zhí)行過程上是有區(qū)別的。并發(fā)是多個(gè)任務(wù)交替執(zhí)行,多任務(wù)之間還是串行的;而并行是多個(gè)任務(wù)同時(shí)執(zhí)行,和并發(fā)有本質(zhì)區(qū)別。

        對(duì)計(jì)算機(jī)而言,如果系統(tǒng)內(nèi)只有一個(gè) CPU ,而使用多進(jìn)程或者多線程執(zhí)行任務(wù),那么這種情況下多線程或者多進(jìn)程就是并發(fā)執(zhí)行,并行只可能出現(xiàn)在多核系統(tǒng)中。當(dāng)然,對(duì) Java 程序而言,我們不必去關(guān)心程序是并行還是并發(fā)。

        臨界區(qū)

        臨界區(qū)表示的是多個(gè)線程共享但同時(shí)只能有一個(gè)線程使用它的資源。在并行程序中臨界區(qū)資源是受保護(hù)的,必須確保同一時(shí)刻只有一個(gè)線程能使用它。

        阻塞

        如果一個(gè)線程占有了臨界區(qū)的資源,其他需要使用這個(gè)臨界區(qū)資源的線程必須在這個(gè)臨界區(qū)進(jìn)行等待(線程被掛起),這種情況就是發(fā)生了阻塞(線程停滯不前)。

        死鎖\饑餓\活鎖

        死鎖就是多個(gè)線程需要其他線程的資源才能釋放它所擁有的資源,而其他線程釋放這個(gè)線程需要的資源必須先獲得這個(gè)線程所擁有的資源,這樣造成了矛盾無法解開;如圖1情形就是發(fā)生死鎖現(xiàn)象:

        圖1:生活中的死鎖現(xiàn)象

        活鎖就是兩個(gè)線程互相謙讓資源,結(jié)果就是誰也拿不到資源導(dǎo)致活鎖;就好比過馬路,行人給車讓道,車又給行人讓道,結(jié)果就是車和行人都停在那不走。

        饑餓就是,某個(gè)線程優(yōu)先級(jí)特別低老是拿不到資源,導(dǎo)致這個(gè)線程一直無法執(zhí)行。

        并發(fā)級(jí)別

        并發(fā)級(jí)別分為阻塞,無饑餓,無障礙,無鎖,無等待幾個(gè)級(jí)別;根據(jù)名字我們也能大概猜出這幾個(gè)級(jí)別對(duì)應(yīng)的什么情形;阻塞,無饑餓和無鎖都好理解;我們說一下無障礙和無等待;

        無障礙:無障礙級(jí)別默認(rèn)各個(gè)線程不會(huì)發(fā)生沖突,不會(huì)互相搶占資源,一旦搶占資源就認(rèn)為線程發(fā)生錯(cuò)誤,進(jìn)行回滾。

        無等待:無等待是在無鎖上的進(jìn)一步優(yōu)化,限制每個(gè)線程完成任務(wù)的步數(shù)。

        并行的兩個(gè)定理

        加速比:加速比=優(yōu)化前系統(tǒng)耗時(shí)/優(yōu)化后系統(tǒng)耗時(shí)

        Amdahl 定理:加速比=1/[F+(1-F)/n] 其中 n 表示處理器個(gè)數(shù) ,F(xiàn)是程序中只能串行執(zhí)行的比例(串行率);由公式可知,想要以最小投入,得到最高加速比即 F+(1-F)/n 取到最小值,F(xiàn) 和 n 都對(duì)結(jié)果有很大影響,在深入研究就是數(shù)學(xué)問題了;

        Gustafson 定律:加速比=n-F(n-1),這兩定律區(qū)別不大,都體現(xiàn)了單純的減少串行率,或者單純的加 CPU 都無法得到最優(yōu)解。

        Java 中的并行基礎(chǔ)

        原子性,可見性,有序性

        原子性指的是一個(gè)操作是不可中斷的,要么成功要么失敗,不會(huì)被其他線程所干擾;比如 int=1 ,這一操作在 cpu 中分為好幾個(gè)指令,但對(duì)程序而言這幾個(gè)指令是一體的,只有可能執(zhí)行成功或者失敗,不可能發(fā)生只執(zhí)行了一半的操作;對(duì)不同 CPU 而言保證原子性的的實(shí)現(xiàn)方式各有不同,就英特爾 CPU 而言是使用一個(gè) lock 指令來保證的。

        可見性指某一線程改變某一共享變量,其他線程未必會(huì)馬上知道。

        有序性指對(duì)一個(gè)操作而言指令是按一定順序執(zhí)行的,但編譯器為了提高程序執(zhí)行的速度,會(huì)重排程序指令;cpu在執(zhí)行指令的時(shí)候采用的是流水線的形式,上一個(gè)指令和下一個(gè)指令差一個(gè)工步。比如A指令分三個(gè)工步:

        1. 操作內(nèi)存a;
        2. 操作內(nèi)存b;
        3. 操作內(nèi)存c;

        現(xiàn)假設(shè)有個(gè)指令 B 操作流程和 A 一樣,那么先執(zhí)行指令 A 再執(zhí)行指令 B 時(shí)間全利用上了,中間沒有停頓等待;但如果有三個(gè)這樣的指令在流水線上執(zhí)行:a>b>cb>e>c ,c>e>a ;這樣的指令順序就會(huì)發(fā)生等待降低了 CPU 的效率,編譯器為了避免這種事情發(fā)生,會(huì)適當(dāng)優(yōu)化指令的順序進(jìn)行重排。

        volatile關(guān)鍵字

        volatile 關(guān)鍵字在 Java 中的作用是保證變量的可見性和防止指令重排。

        線程的相關(guān)操作

        創(chuàng)建線程有三種方法

        • 繼承Thread類創(chuàng)建線程
        • 實(shí)現(xiàn)Runnable接口創(chuàng)建線程
        • 使用Callable和Future創(chuàng)建線程

        終止線程的方法

        終止線程可調(diào)用 stop() 方法,但這個(gè)方法是被廢棄不建議使用的,因?yàn)閺?qiáng)制終止一個(gè)線程會(huì)引起數(shù)據(jù)的不一致問題。比如一個(gè)線程數(shù)據(jù)寫到一半被終止了,釋放了鎖,其他線程拿到鎖繼續(xù)寫數(shù)據(jù),結(jié)果導(dǎo)致數(shù)據(jù)發(fā)生了錯(cuò)誤。終止線程比較好的方法是“讓程序自己終止”,比如定義一個(gè)標(biāo)識(shí)符,當(dāng)標(biāo)識(shí)符為 true 的時(shí)候直讓程序走到終點(diǎn),這樣就能達(dá)到“自己終止”的目的。

        線程的中斷等待和通知

        interrupt() 方法可以中斷當(dāng)前程序,object.wait() 方法讓線程進(jìn)入等待隊(duì)列,object.notify() 隨機(jī)喚醒等待隊(duì)列的一個(gè)線程, object.notifyAll() 喚醒等待隊(duì)列的所有線程。object.wait() 必須在 synchronzied ?語句中調(diào)用;執(zhí)行wait、notify 方法必須獲得對(duì)象的監(jiān)視器,執(zhí)行結(jié)束后釋放監(jiān)視器供其他線程獲取。

        join

        join() 方法功能是等待其他線程“加入”,可以理解為將某個(gè)線程并為自己的子線程,等子線程走完或者等子線程走規(guī)定的時(shí)間,主線程才往下走;join 的本質(zhì)是調(diào)用調(diào)用線程對(duì)象的 wait 方法,當(dāng)我們執(zhí)行 wait 或者 notify 方法不應(yīng)該獲取線程對(duì)象的監(jiān)聽器,因?yàn)榭赡軙?huì)影響到其他線程的 join。

        yield

        yield 是線程的“謙讓”機(jī)制,可以理解為當(dāng)線程搶到 cpu 資源時(shí),放棄這次資源重新?lián)屨迹瑈ield() 是 Thread 里的一個(gè)靜態(tài)方法。

        線程組

        如果一個(gè)多線程系統(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)建了一個(gè) testGroup 線程組。

        守護(hù)線程

        守護(hù)線程是一種特殊線程,它類似 Java 中的異常系統(tǒng),主要是概念上的分類,與之對(duì)應(yīng)的是用戶線程。它功能應(yīng)該是在后臺(tái)完成一些系統(tǒng)性的服務(wù);設(shè)置一個(gè)線程為守護(hù)線程應(yīng)該在線程 start 之前 setDaemon()。

        線程優(yōu)先級(jí)

        Java 中線程可以有自己的優(yōu)先級(jí),優(yōu)先級(jí)高的更有優(yōu)勢(shì)搶占資源;線程優(yōu)先級(jí)高的不一定能搶占到資源,只是一個(gè)概率問題,而對(duì)應(yīng)優(yōu)先級(jí)低的線程可能會(huì)發(fā)生饑餓。

        在 Java 中使用1到10表示線程的優(yōu)先級(jí),使用setPriority()方法來進(jìn)行設(shè)置,數(shù)字越大代表優(yōu)先級(jí)越高。

        Java 線程鎖

        以下分類是從多個(gè)同角度來劃分,而不是以某一標(biāo)準(zhǔn)來劃分,請(qǐng)注意:

        • 阻塞鎖:當(dāng)一個(gè)線程獲得鎖,其他線程就會(huì)被阻塞掛起,直到搶占到鎖才繼續(xù)執(zhí)行,這樣會(huì)導(dǎo)致 CPU 切換上下文,切換上下文對(duì) CPU 而言是很耗費(fèi)時(shí)間的。
        • 非阻塞鎖:當(dāng)一個(gè)線程獲得鎖,其他線程直接跳過鎖資源相關(guān)的代碼繼續(xù)執(zhí)行,就是非阻塞鎖。
        • 自旋鎖:當(dāng)一個(gè)線程獲得鎖,其他線程則在不停進(jìn)行空循環(huán),直到搶到鎖,這樣做的好處是避免了上下文切換。
        • 可重入鎖:也叫做遞歸鎖,當(dāng)一個(gè)線程獲得該鎖后,可以多次進(jìn)入該鎖所同步著的代碼塊。
        • 互斥鎖:互斥鎖保證了某一時(shí)刻只能有一個(gè)線程占有該資源。
        • 讀寫鎖:將代碼功能分為讀和寫,讀不互斥,寫互斥。
        • 公平鎖/非公平鎖:公平鎖就是在等待隊(duì)列里排最前面的的先獲得鎖,非公平鎖就是誰搶到誰用。
        • 重量級(jí)鎖/輕量級(jí)鎖/偏向鎖:使用操作系統(tǒng) “Mutex Lock” 功能來實(shí)現(xiàn)鎖機(jī)制的叫重量級(jí)鎖,因?yàn)檫@種鎖成本高;輕量級(jí)鎖是對(duì)重量級(jí)鎖的優(yōu)化,提高性能;偏向鎖是對(duì)輕量級(jí)鎖的優(yōu)化,在無多線程競(jìng)爭(zhēng)的情況下盡量減少不必要的輕量級(jí)鎖執(zhí)行路徑。

        synchronized

        屬于阻塞鎖、互斥鎖、非公平鎖以及可重入鎖,在 JDK1.6 以前屬于重量級(jí)鎖,后來做了優(yōu)化。

        用法:

        • 指定加鎖對(duì)象
        • 用于靜態(tài)代碼塊/方法
        • 用于動(dòng)態(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");
        ????}
        }

        當(dāng)鎖加在靜態(tài)代碼塊上或者靜態(tài)方法上或者為 synchronized (xxx.class){} 時(shí),鎖作用于整個(gè)類,凡是屬于這個(gè)類的對(duì)象的相關(guān)都會(huì)被上鎖,當(dāng)用于動(dòng)態(tài)方法或者為或者為synchronized (object){}時(shí)鎖作用于對(duì)象;除此之外,synchronized可以保證線程的可見性和有序性。

        Lock

        Lock 是一個(gè)接口,其下有多個(gè)實(shí)現(xiàn)類。

        方法說明:

        • lock()方法是平常使用得最多的一個(gè)方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進(jìn)行等待。
        • tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲?。瑒t返回false,這個(gè)方法還可以設(shè)置一個(gè)獲取鎖的等待時(shí)長(zhǎng),如果時(shí)間內(nèi)獲取不到直接返回。
        • 兩個(gè)線程同時(shí)通過lock.lockInterruptibly()想獲取某個(gè)鎖時(shí),假若此時(shí)線程A獲取到了鎖,而線程B只有在等待,那么對(duì)線程B調(diào)用threadB.interrupt()方法能夠中斷線程B的等待過程。
        • unLock()方法是用來釋放鎖。
        • newCondition():生成一個(gè)和線程綁定的Condition實(shí)例,利用該實(shí)例我們可以讓線程在合適的時(shí)候等待,在特定的時(shí)候繼續(xù)執(zhí)行,相當(dāng)于得到這個(gè)線程的wait和notify方法。

        ReentrantLock

        ReentrantLock 重入鎖,是實(shí)現(xiàn) Lock 接口的一個(gè)類,它對(duì)公平鎖和非公平鎖都支持,在構(gòu)造方法中傳入一個(gè) boolean 值,true 時(shí)為公平鎖,false 時(shí)為非公平鎖。

        Semaphore(信號(hào)量)

        信號(hào)量是對(duì)鎖的擴(kuò)展,鎖每次只允許一個(gè)線程訪問一個(gè)資源,而信號(hào)量卻可以指定多個(gè)線程訪問某個(gè)資源,信號(hào)量的構(gòu)造函數(shù)為

        public?Semaphore(int?permits)?{
        ???sync?=?new?NonfairSync(permits);
        }
        public?Semaphore(int?permits,?boolean?fair)?{
        ???sync?=?fair???new?FairSync(permits)?:?new?NonfairSync(permits);
        }

        第一個(gè)方法指定了可使用的線程數(shù),第二個(gè)方法的布爾值表示是否為公平鎖。

        acquire() 方法嘗試獲得一個(gè)許可,如果獲取不到則等待;tryAcquire() 方法嘗試獲取一個(gè)許可,成功返回 true,失敗返回false,不會(huì)阻塞,tryAcquire(int i) 指定等待時(shí)間;release() 方法釋放一個(gè)許可。

        ReadWriteLock

        讀寫分離鎖, 讀寫分離鎖可以有效的減少鎖競(jìng)爭(zhēng),讀鎖是共享鎖,可以被多個(gè)線程同時(shí)獲取,寫鎖是互斥只能被一個(gè)線程占有,ReadWriteLock 是一個(gè)接口,其中 readLock() 獲得讀鎖,writeLock() 獲得寫鎖 其實(shí)現(xiàn)類 ReentrantReadWriteLock 是一個(gè)可重入得的讀寫鎖,它支持鎖的降級(jí)(在獲得寫鎖的情況下可以再持有讀鎖),不支持鎖的升級(jí)(在獲得讀鎖的情況下不能再獲得寫鎖);讀鎖和寫鎖也是互斥的,也就是一個(gè)資源要么被上了一個(gè)寫鎖,要么被上了多個(gè)讀鎖,不會(huì)發(fā)生這個(gè)資即被上寫鎖又被上讀鎖的情況。

        cas

        cas(比較替換):無鎖策略的一種實(shí)現(xiàn)方式,過程為獲取到變量舊值(每個(gè)線程都有一份變量值的副本),和變量目前的新值做比較,如果一樣證明變量沒被其他線程修改過,這個(gè)線程就可以更新這個(gè)變量,否則不能更新;通俗的說就是通過不加鎖的方式來修改共享資源并同時(shí)保證安全性。

        使用cas的話對(duì)于屬性變量不能再用傳統(tǒng)的 int ,long 等;要使用原子類代替原先的數(shù)據(jù)類型操作,比如 AtomicBoolean,AtomicInteger,AtomicInteger 等。

        并發(fā)下集合類

        并發(fā)集合類主要有:

        • ConcurrentHashMap:支持多線程的分段哈希表,它通過將整個(gè)哈希表分成多段的方式減小鎖粒度。
        • ConcurrentSkipListMap:ConcurrentSkipListMap的底層是通過跳表來實(shí)現(xiàn)的。跳表是一個(gè)鏈表,但是通過使用“跳躍式”查找的方式使得插入、讀取數(shù)據(jù)時(shí)復(fù)雜度變成了O(logn)。
        • ConCurrentSkipListSet:參考 ConcurrentSkipListMap。
        • CopyOnWriteArrayList:是 ArrayList 的一個(gè)線程安全的變形,其中所有可變操作(添加、設(shè)置,等等)都是通過對(duì)基礎(chǔ)數(shù)組進(jìn)行一次新的復(fù)制來實(shí)現(xiàn)的。
        • CopyOnWriteArraySet:參考 CopyOnWriteArrayList。
        • ConcurrentLinkedQueue:cas 實(shí)現(xiàn)的非阻塞并發(fā)隊(duì)列。

        線程池

        介紹

        多線程的設(shè)計(jì)優(yōu)點(diǎn)是能很大限度的發(fā)揮多核處理器的計(jì)算能力,但是,若不控制好線程資源反而會(huì)拖累cpu,降低系統(tǒng)性能,這就涉及到了線程的回收復(fù)用等一系列問題;而且本身線程的創(chuàng)建和銷毀也很耗費(fèi)資源,因此找到一個(gè)合適的方法來提高線程的復(fù)用就很必要了。

        線程池就是解決這類問題的一個(gè)很好的方法:線程池中本身有很多個(gè)線程,當(dāng)需要使用線程的時(shí)候拿一個(gè)線程出來,當(dāng)用完則還回去,而不是每次都創(chuàng)建和銷毀。在 JDK 中提供了一套 Executor 線程池框架,幫助開發(fā)人員有效的進(jìn)行線程控制。

        Executor 使用

        獲得線程池的方法:

        • newFixedThreadPool(int nThreads) :創(chuàng)建固定數(shù)目線程的線程池。
        • newCachedThreadPool:創(chuàng)建一個(gè)可緩存的線程池,調(diào)用execute將重用以前構(gòu)造的線程(如果線程可用)。如果現(xiàn)有線程沒有可用的,則創(chuàng)建一個(gè)新線 程并添加到池中。
        • newSingleThreadExecutor:創(chuàng)建一個(gè)單線程化的 Executor。
        • newScheduledThreadPool:創(chuàng)建一個(gè)支持定時(shí)及周期性的任務(wù)執(zhí)行的線程池。

        以上方法都是返回一個(gè) ExecutorService 對(duì)象,executorService.execute() 傳入一個(gè) Runnable 對(duì)象,可執(zhí)行一個(gè)線程任務(wù)。

        下面看示例代碼

        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);
        ??}
        ?}
        }

        線程池是一個(gè)龐大而復(fù)雜的體系,本文定位是基礎(chǔ),不對(duì)其做更深入的研究,感興趣的小伙伴可以自行查資料進(jìn)行學(xué)習(xí)。

        ScheduledExecutorService

        newScheduledThreadPool(int corePoolSize) 會(huì)返回一個(gè)ScheduledExecutorService 對(duì)象,可以根據(jù)時(shí)間對(duì)線程進(jìn)行調(diào)度;其下有三個(gè)執(zhí)行線程任務(wù)的方法:schedule(),scheduleAtFixedRate() 以及 scheduleWithFixedDelay() 該線程池可解決定時(shí)任務(wù)的問題。

        示例:

        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);//延時(shí)waits?執(zhí)行
        ????}
        }

        job1的執(zhí)行方式是任務(wù)發(fā)起后間隔 wait 秒開始執(zhí)行,每隔 period 秒(注意:不包括上一個(gè)線程的執(zhí)行時(shí)間)執(zhí)行一次;

        job2的執(zhí)行方式是任務(wù)發(fā)起后間隔 wait 秒開始執(zhí)行,等線程結(jié)束后隔 period 秒開始執(zhí)行下一個(gè)線程;

        job3只執(zhí)行一次,延遲 wait 秒執(zhí)行;

        ScheduledExecutorService 還可以配合 Callable 使用來回調(diào)獲得線程執(zhí)行結(jié)果,還可以取消隊(duì)列中的執(zhí)行任務(wù)等操作,這屬于比較復(fù)雜的用法,我們這里掌握基本的即可,到實(shí)際遇到相應(yīng)的問題時(shí)我們?cè)诂F(xiàn)學(xué)現(xiàn)用,節(jié)省學(xué)習(xí)成本。

        鎖優(yōu)化

        減小鎖持有時(shí)間

        減小鎖的持有時(shí)間可有效的減少鎖的競(jìng)爭(zhēng)。如果線程持有鎖的時(shí)間越長(zhǎng),那么鎖的競(jìng)爭(zhēng)程度就會(huì)越激烈。因此,應(yīng)盡可能減少線程對(duì)某個(gè)鎖的占有時(shí)間,進(jìn)而減少線程間互斥的可能。

        減少鎖持有時(shí)間的方法有:

        • 進(jìn)行條件判斷,只對(duì)必要的情況進(jìn)行加鎖,而不是整個(gè)方法加鎖。
        • 減少加鎖代碼的行數(shù),只對(duì)必要的步驟加鎖。

        減小鎖粒度

        減小鎖的范圍,減少鎖住的代碼行數(shù)可減少鎖范圍,減小共享資源的范圍也可減小鎖的范圍。減小鎖共享資源的范圍的方式比較常見的有分段鎖,比如 ConcurrentHashMap ,它將數(shù)據(jù)分為了多段,當(dāng)需要 put 元素的時(shí)候,并不是對(duì)整個(gè) hashmap 進(jìn)行加鎖,而是先通過 hashcode 來知道他要放在那一個(gè)分段中,然后對(duì)這個(gè)分段進(jìn)行加鎖,所以當(dāng)多線程 put 的時(shí)候,只要不是放在一個(gè)分段中,就實(shí)現(xiàn)了真正的并行的插入。

        鎖分離

        鎖分離最常見的操作就是讀寫分離了,讀寫分離的操作參考 ReadWriteLock 章節(jié),而對(duì)讀寫分離進(jìn)一步的延伸就是鎖分離了。為了提高線程的并行量,我們可以針對(duì)不同的功能采用不同的鎖,而不是統(tǒng)統(tǒng)用同一把鎖。比如說有一個(gè)同步方法未進(jìn)行鎖分離之前,它只有一把鎖,任何線程來了,只有拿到鎖才有資格運(yùn)行,進(jìn)行鎖分離之后就不是這種情形了——來一個(gè)線程,先判斷一下它要干嘛,然后發(fā)一個(gè)對(duì)應(yīng)的鎖給它,這樣就能一定程度上提高線程的并行數(shù)。

        鎖粗化

        一般為了保證多線程間的有效并發(fā),會(huì)要求每個(gè)線程持有鎖的時(shí)間盡量短,也就是說鎖住的代碼盡量少。但是如果如果對(duì)同一個(gè)鎖不停的進(jìn)行請(qǐng)求、同步和釋放,其本身也會(huì)消耗系統(tǒng)寶貴的資源,反而不利于性能的優(yōu)化 。比如有三個(gè)步驟:a、b、c,a同步,b不同步,c同步;那么一個(gè)線程來時(shí)候會(huì)上鎖釋放鎖然后又上鎖釋放鎖。這樣反而可能會(huì)降低線程的執(zhí)行效率,這個(gè)時(shí)候我們將鎖粗化可能會(huì)更好——執(zhí)行 a 的時(shí)候上鎖,執(zhí)行完 c 再釋放鎖。

        鎖擴(kuò)展

        分布式鎖

        JDK 提供的鎖在單體項(xiàng)目中不會(huì)有什么問題,但是在集群項(xiàng)目中就會(huì)有問題了。在分布式模型下,數(shù)據(jù)只有一份(或有限制),此時(shí)需要利用鎖的技術(shù)控制某一時(shí)刻修改數(shù)據(jù)的進(jìn)程數(shù)。JDK 鎖顯然無法滿足我們的需求,于是就有了分布式鎖。

        分布式鎖的實(shí)現(xiàn)有三種方式:

        • 基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖
        • 基于緩存(redis,memcached,tair)實(shí)現(xiàn)分布式鎖
        • 基于 Zookeeper 實(shí)現(xiàn)分布式鎖

        基于redis的分布式鎖比較使用普遍,在這里介紹其原理和使用:

        redis 實(shí)現(xiàn)鎖的機(jī)制是 setnx 指令,setnx 是原子操作命令,鎖存在不能設(shè)置值,返回 0 ;鎖不存在,則設(shè)置鎖,返回 1 ,根據(jù)返回值來判斷上鎖是否成功。看到這里你可能想為啥不先 get 有沒有值,再 set 上鎖;首先我們要知道,redis 是單線程的,同一時(shí)刻只可能有一個(gè)線程操作內(nèi)存,然后 setnx 是一個(gè)操作步驟(具有原子性),而 get 再 set 是兩個(gè)步驟(不具有原子性)。如果使用第二種可能會(huì)發(fā)生這種情況:客戶端 a get發(fā)現(xiàn)沒有鎖,這個(gè)時(shí)候被切換到客戶端b,b get也發(fā)現(xiàn)沒鎖,然后b set,這個(gè)時(shí)候又切換到a客戶端 a set;這種情況下,鎖完全沒起作用。所以,redis分布式鎖,原子性是關(guān)鍵。

        對(duì)于 web 應(yīng)用中 redis 客戶端用的比較多的是 lettuce,jedis,redisson。springboot 的 redis 的 start 包底層是 lettuce ,但對(duì) redis 分布式鎖支持得最好的是 redisson(如果用 redisson 你就享受不到 redis 自動(dòng)化配置的好處了);不過 springboot 的 redisTemplete 支持手寫 lua 腳本,我們可以通過手寫 lua 腳本來實(shí)現(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;
        }

        關(guān)于lua腳本的語法我就不做介紹了。

        在 github 上也有開源的 redis 鎖項(xiàng)目,比如 spring-boot-klock-starter 感興趣的小伙伴可以去試用一下。

        數(shù)據(jù)庫(kù)鎖

        對(duì)于存在多線程問題的項(xiàng)目,比如商品貨物的進(jìn)銷存,訂單系統(tǒng)單據(jù)流轉(zhuǎn)這種,我們可以通過代碼上鎖來控制并發(fā),也可以使用數(shù)據(jù)庫(kù)鎖來控制并發(fā),數(shù)據(jù)庫(kù)鎖從機(jī)制上來說分樂觀鎖和悲觀鎖。

        悲觀鎖:

        悲觀鎖分為共享鎖(S鎖)和排他鎖(X鎖),MySQL 數(shù)據(jù)庫(kù)讀操作分為三種——快照讀,當(dāng)前讀;快照讀就是普通的讀操作,如:

        select?*from?table

        當(dāng)前讀就是對(duì)數(shù)據(jù)庫(kù)上悲觀鎖了;其中 select ... lock in share mode 屬于共享鎖,多個(gè)事務(wù)對(duì)于同一數(shù)據(jù)可以共享,但只能讀不能修改。而下面三種 SQL :

        select?...for?update
        update?...?set...
        insert?into?...

        屬于排他鎖,排他鎖就是不能與其他鎖并存,如一個(gè)事務(wù)獲取了一個(gè)數(shù)據(jù)行的排他鎖,其他事務(wù)就不能再獲取該行的其他鎖,包括共享鎖和排他鎖,但是獲取排他鎖的事務(wù)是可以對(duì)數(shù)據(jù)行讀取和修改,排他鎖是阻塞鎖。

        樂觀鎖:

        就是很樂觀,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人不會(huì)修改,所以不會(huì)上鎖,但是在更新的時(shí)候會(huì)判斷一下在此期間別人有沒有去更新這個(gè)數(shù)據(jù),如果有則更新失敗。一種實(shí)現(xiàn)方式為在數(shù)據(jù)庫(kù)表中加一個(gè)版本號(hào)字段 version ,任何 update 語句 where 后面都要跟上 version=?,并且每次 update 版本號(hào)都加 1。如果 a 線程要修改某條數(shù)據(jù),它需要先 select 快照讀獲得版本號(hào),然后 update ,同時(shí)版本號(hào)加一。這樣就保證了在 a 線程修改某條數(shù)據(jù)的時(shí)候,確保其他線程沒有修改過這條數(shù)據(jù),一旦其他線程修改過,就會(huì)導(dǎo)致 a 線程版本號(hào)對(duì)不上而更新失?。ㄟ@其實(shí)是一個(gè)簡(jiǎn)化版的mvcc)。

        樂觀鎖適用于允許更新失敗的業(yè)務(wù)場(chǎng)景,悲觀鎖適用于確保更新操作被執(zhí)行的場(chǎng)景。

        并發(fā)編程相關(guān)

        • 善用 Java8 Stream
        • 對(duì)于生產(chǎn)者消費(fèi)者模式,條件判斷是使用 while 而不是 if
        • 懶漢單例采用雙重檢查和鎖保證線程安全
        • 善用 Future 模式
        • 合理使用 ThreadLocal

        Java 8 引入 lambda 表達(dá)式使在 Java 中使用函數(shù)式編程很方便。而 Java 8 中的 stream 對(duì)數(shù)據(jù)的處理能使線程執(zhí)行速度得以優(yōu)化。Future 模式是一種對(duì)異步線程的回調(diào)機(jī)制;現(xiàn)在 cpu 都是多核的,我們?cè)谔幚硪恍┹^為費(fèi)時(shí)的任務(wù)時(shí)可使用異步,在后臺(tái)開啟多個(gè)線程同時(shí)處理,等到異步線程處理完再通過 Future 回調(diào)拿到處理的結(jié)果。

        ThreadLocal 的實(shí)例代表了一個(gè)線程局部的變量,每條線程都只能看到自己的值,并不會(huì)意識(shí)到其它的線程中也存在該變量(這里原理就不說了,網(wǎng)上資料很多),總之就是我們?nèi)绻朐诙嗑€程的類里面使用線程安全的變量就用 ThreadLocal ,但是請(qǐng)一定要注意用完記得 remove ,不然會(huì)發(fā)生內(nèi)存泄漏。

        總結(jié)

        隨著后端發(fā)展,現(xiàn)在單體項(xiàng)目越來越少,基本上都是集群和分布式,這樣也使得 JDK ?的鎖慢慢變得無用武之地。但是萬變不離其宗,雖然鎖的實(shí)現(xiàn)方式變了,但其機(jī)制是沒變的;無論是分布式鎖還是 JDK 鎖,其目的和處理方式都是一個(gè)機(jī)制,只是處理對(duì)象不一樣而已。

        我們?cè)谄綍r(shí)編寫程序時(shí)對(duì)多線程最應(yīng)該注意的就是線程優(yōu)化和鎖問題。我們腦中要對(duì)鎖機(jī)制有一套體系,而對(duì)線程的優(yōu)化經(jīng)驗(yàn)在于平時(shí)的積累和留心

        —?【 THE END 】—
        本公眾號(hào)全部博文已整理成一個(gè)目錄,請(qǐng)?jiān)诠娞?hào)里回復(fù)「m」獲取!


        3T技術(shù)資源大放送!包括但不限于:Java、C/C++,Linux,Python,大數(shù)據(jù),人工智能等等。在公眾號(hào)內(nèi)回復(fù)「1024」,即可免費(fèi)獲取??!




        瀏覽 51
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 日日射夜夜操 | 久久不雅视频 | 日韩成人精品 | 婷婷色丁香五月 | 中文字幕日韩精品人妻在线视频 |