并發(fā)編程基礎(chǔ)概念

CPU高速緩存、CPU多級(jí)緩存、CPU寄存器和CPU亂序執(zhí)行優(yōu)化

由于cpu的運(yùn)算速度遠(yuǎn)遠(yuǎn)大于主內(nèi)存的工作速度,為了能讓主內(nèi)存不要太落后cpu,引入了高速緩存,隨著計(jì)算機(jī)的復(fù)雜程度的提升,高速緩存與主存的速度差異越來越大,隨后加入了多級(jí)緩存:三級(jí)緩存。由此進(jìn)一步緩解cpu與主存的速度差異。
image.png

CPU寄存器也是屬于內(nèi)存,CPU內(nèi)部也是需要內(nèi)存的,CUP在這部分內(nèi)存操作的速度遠(yuǎn)遠(yuǎn)大于在主內(nèi)存和高速緩存中的操作速度,其實(shí)CPU與主內(nèi)存互不認(rèn)識(shí),它們之間的交互都交給高速緩存和CPU寄存器,高速緩存充當(dāng)了寄存器與主內(nèi)存的交互地帶,每當(dāng)數(shù)據(jù)需要被CPU計(jì)算時(shí),先從主內(nèi)存中的數(shù)據(jù)讀到高速緩存中,再從高速緩存讀取到寄存器中讓CPU處理,CPU處理完數(shù)據(jù)后返回主內(nèi)存也需要讓寄存器把數(shù)據(jù)拷貝到高速緩存中然后主內(nèi)存才能讀取到CPU處理完事的數(shù)據(jù)
image.png

指令重排

為了充分提升cpu性能,cup可以對(duì)代碼的亂序執(zhí)行,在單核單線程情況下這種這種機(jī)制能保證運(yùn)算結(jié)果與代碼順序執(zhí)行后的結(jié)果一致,但是多線程情況下就會(huì)出現(xiàn)結(jié)果不一致的情況。這個(gè)也稱作cpu的指令重排

JVM堆棧和計(jì)算機(jī)結(jié)構(gòu)的關(guān)系

存在堆上的對(duì)象可以被棧上的持有這個(gè)對(duì)象引用的方法所訪問,如果棧上兩個(gè)方法都擁有了這個(gè)對(duì)象的引用,可能會(huì)發(fā)生并發(fā)問題,因?yàn)闂I系木€程擁有了這個(gè)對(duì)象的私有拷貝
image.png
image.png

JMM與并發(fā):

主存:計(jì)算機(jī)主內(nèi)存,堆棧主要就是存在主內(nèi)存中,堆棧內(nèi)存屬于主內(nèi)存的一部分

工作內(nèi)存:工作內(nèi)存就是開辟給線程處理數(shù)據(jù)的內(nèi)存空間類似CPU的寄存器我們也認(rèn)為它就是寄存器和高速緩存,工作內(nèi)存其實(shí)是一個(gè)不存在的邏輯區(qū)域,這是把線程與主內(nèi)存間的交互區(qū)域給抽象出來,在JVM內(nèi)存模型的角度來看我們還可以認(rèn)為它就是線程的棧內(nèi)存,里面存放的是棧內(nèi)每個(gè)線程的對(duì)堆對(duì)象的私有拷貝和線程所要執(zhí)行的方法的局部變量
image.png

由于線程是將工作內(nèi)存的共享變量拷貝到工作內(nèi)存進(jìn)行處理,每個(gè)線程的工作內(nèi)存都是線程私有的,所以線程之間的操作是互相不可見的,因此會(huì)產(chǎn)生線程并發(fā)問題

JMM同步的八大操作:

  • read:是將主內(nèi)存的變量讀取出來

  • locad:將主內(nèi)存read到的變量加載進(jìn)工作內(nèi)存中

  • use:將工作內(nèi)存的變量傳遞給執(zhí)行引擎

  • assign:從執(zhí)行引擎中接收變量

  • store: 將assign到的變量存儲(chǔ)到工作內(nèi)存中

  • write: 將工作內(nèi)存中的變量寫入主內(nèi)存

  • lock:將主內(nèi)存的變量標(biāo)識(shí)為線程獨(dú)占

  • unlock:將主內(nèi)存處于lock的變量釋放。

jmm并發(fā)八大操作

八大操作的規(guī)則:

1、read與load不允許單獨(dú)出現(xiàn)、write與store也不允許單獨(dú)出現(xiàn),并且它們必須是按順序的操作,不允許只read不load或者只store不write,只有read完后才能load,只有store完之后才能write,但是沒有要求他們連續(xù)執(zhí)行中間是可以穿插其他的指令操作的,兩套操作具有原子性。
2、變量在工作內(nèi)存中發(fā)生變化,必須更新到主內(nèi)存中,新的變量只能在主內(nèi)存中產(chǎn)生,不允許在工作內(nèi)存中使用未被初始化的變量,就是在store和use前必須做assign和use,通過load或者assign才能拿到變量初始化的值。
3、一個(gè)變量同一時(shí)刻只允許一個(gè)線程執(zhí)行l(wèi)ock操作這個(gè)是同步性,同一個(gè)lock允許被同一條線程執(zhí)行多次,但是必須執(zhí)行相同次數(shù)的unlock變量才被解鎖,這就是可重入性。
4、變量被執(zhí)行l(wèi)ock時(shí),將清空工作內(nèi)存中此變量的值,執(zhí)行引擎使用此變量之前,需重新執(zhí)行l(wèi)oad操作,這就是可見性
5、當(dāng)對(duì)變量進(jìn)行unlock操作時(shí)之前,必須將此變量從工作內(nèi)存中同步回主內(nèi)存。

線程安全性:

阻塞同步:又稱悲觀鎖,主要是使同一時(shí)刻只有獲取鎖的線程才能進(jìn)行操作,其他線程只能阻塞,如synchronized和ReentrantLock都是阻塞同步
非阻塞同步:又稱樂觀鎖,全程不上鎖,主要是在修改數(shù)據(jù)前通過檢測(cè)是否有其他線程競爭,有則放棄修改,采取其他措施補(bǔ)償,如重試、自旋等,CAS就是非阻塞同步的

線程安全性的三點(diǎn)特性:

synchronized以及ReentrantLock都是擁有這些特性的

原子性:

提供互斥訪問,同一時(shí)刻只允許一個(gè)線程進(jìn)行對(duì)它進(jìn)行操作操作
代碼演示:
原子類AtomicIntegerFieldUpdater實(shí)現(xiàn)一個(gè)CAS

public class BingFa2 {
    private static ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*2+1);
    public volatile int count=0;
    private static AtomicIntegerFieldUpdater<BingFa2> updater=
            AtomicIntegerFieldUpdater.newUpdater(BingFa2.class,"count");
    CountDownLatch countDownLatch = new CountDownLatch(1000);

    static BingFa2 bingFa2=new BingFa2();
    public void test1() throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            executorService.submit(() -> {
                int qw;
                do {
                  //從底層拿值,期望值
                     qw = updater.get(bingFa2);
                  //加一
                }while (!updater.compareAndSet(bingFa2,qw,count+1));
              countDownLatch.countDown();
            });
        }
        executorService.shutdown();
        countDownLatch.await();
        System.out.println(updater.get(bingFa2));
    }

    public static void main(String[] args) throws InterruptedException {
        bingFa2.test1();
    }
}
線程安全

原子類AtomicReference

public class AtomicReferenceTest {
    private static AtomicReference<Integer> compare =
            new AtomicReference<>(0);
    public void test1() throws InterruptedException {
        //比較值后在賦值,賦值成功則返回true,否則false
        System.out.println(compare.compareAndSet(0, 1));//true
        System.out.println(compare.compareAndSet(2, 1));//false
        System.out.println(compare.compareAndSet(1, 3));//true
        System.out.println(compare.get());
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicReferenceTest atomicReferenceTest = new AtomicReferenceTest();
        atomicReferenceTest.test1();
    }
}
底層是cas

LongAdder的演示,LongAdder在并發(fā)高的時(shí)候可以分散熱點(diǎn)數(shù)據(jù)性能較好,但是并發(fā)很高時(shí)數(shù)據(jù)可能不一致,所以要求數(shù)據(jù)精準(zhǔn)的場(chǎng)景不要用它,但是在并發(fā)高很高的場(chǎng)景要求性能高的使用它比較好,并發(fā)很多低的還是用其他的Atomic類比較好,如AtomicLong,AtomicInteger

public class LongAddTest {
    private static ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*2+1);
    private static LongAdder longAdder=new LongAdder();
    CountDownLatch countDownLatch = new CountDownLatch(1000);
    public void test1() throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            executorService.submit(() -> {
             //加一
                longAdder.increment();
                countDownLatch.countDown();
            });
        }
        executorService.shutdown();
        countDownLatch.await();
        System.out.println(longAdder.longValue());
    }

    public static void main(String[] args) throws InterruptedException {
        LongAddTest longAddTest=new LongAddTest();
        longAddTest.test1();
    }
}
線程安全
CAS的ABA問題:

ABA問題就是在CAS操作時(shí),其他線程將變量由A改為了B,然后又改回了A,這時(shí)CAS從主內(nèi)存中得到的變量是A值和工作內(nèi)存中的值一樣,這個(gè)結(jié)果雖然一樣,但是畢竟被其他線程修改過,和原子性思想不符,解決這個(gè)問題只需要在變量的版本號(hào)加1,每次修改變量都會(huì)修改版本號(hào),這樣就很好的解決了ABA問題

可見性:

一個(gè)線程對(duì)主內(nèi)存的修改可以及時(shí)被其他線程觀察到

造成共享變量不可見的原因有三點(diǎn):

1、線程的交叉執(zhí)行 2、指令重排序加線程交叉執(zhí)行 3、工作內(nèi)存中的共享變量發(fā)生變化沒有及時(shí)更新到主內(nèi)存中?;蛘呤侵鲀?nèi)存的共享變量發(fā)生變化沒有更新值工作內(nèi)存中

可見性的實(shí)現(xiàn)手段:
synchronized:
這個(gè)其實(shí)我在上面的jmm八大操作的lock和unlock中就提到了:

  • 解鎖:線程解鎖前必須要把共享變量的最新值刷新到主內(nèi)存中

  • 加鎖:線程必須把工作內(nèi)存的共享變量清理,把使用到的共享變量從主內(nèi)存中更新到工作內(nèi)存中

volatile:
禁止cup的指令重排優(yōu)化,在對(duì)volatile變量寫操作時(shí),加入一個(gè)store屏障,禁止指令的重排,強(qiáng)制將變量的更新刷新到主內(nèi)存中,在對(duì)volatile變量讀操作時(shí),加入一個(gè)load屏障,禁止指令的重排,強(qiáng)制的將主內(nèi)存的變量刷新到工作內(nèi)存中,因此volatile保證了線程的可見性

線程在讀volatile變量時(shí)會(huì)將本地內(nèi)存的變量值置為無效,重新讀取主內(nèi)存的值,線程在寫volatile變量后會(huì)將本地內(nèi)存的成員變量的值刷新到主內(nèi)存中,如此實(shí)現(xiàn)了線程之間的可見性

volatile能實(shí)現(xiàn)線程對(duì)一個(gè)變量修改后的可見性,但是無法保證兩個(gè)線程同時(shí)操作一個(gè)變量的發(fā)生的不安全現(xiàn)象,volatile依舊是線程不安全的

一、volatile關(guān)鍵字的作用

1、保證變量寫操作的可見性;
2、保證變量前后代碼的執(zhí)行順序;

二、volatile的底層原理
被volatile修飾的變量被修改時(shí),會(huì)將修改后的變量直接寫入主存中,并且將其他線程中該變量的緩存置為無效,從而讓其它線程對(duì)該變量的引用直接從主存中獲取數(shù)據(jù),這樣就保證了變量的可見性。 但是volatile修飾的變量在自增時(shí)由于該操作分為讀寫兩個(gè)步驟,所以當(dāng)一個(gè)線程的讀操作被阻塞時(shí),另一個(gè)線程同時(shí)也進(jìn)行了自增操作,此時(shí)由于第一個(gè)線程的寫操作沒有進(jìn)行所以主存中仍舊是之前的原數(shù)據(jù),所以當(dāng)兩個(gè)線程自增完成后,該變量可能只加了1。因而volatile是無法保證對(duì)變量的任何操作都是原子性的。

三、volatile的適用場(chǎng)景

1、變量的修改不依賴于變量本身
像是i++、i+=這類的操作在多線程下都是不能保證變量的原子性的。
2、該變量沒有包含在具有其他變量的不變式中

happens-before:
要保證線程的可見性,則線程之間要存在happens-before原則:

happens-before

有序性:

程序執(zhí)行的順序按照代碼的先后順序執(zhí)行

多線程下指令重排序會(huì)造成線程不安全:

public class Singleton {
    private static Singleton unsafeObject=null;
    //線程不安全,發(fā)生了指令重排,原因是unsafeObject是不可見的
    public static Singleton unsafeObject(){
        if (unsafeObject==null){
            synchronized (Singleton.class){
                if (unsafeObject==null) {
                    return new Singleton();
                }
            }
        }
        return unsafeObject;
    }
}
    //Singleton成員變量加上volatile禁止指令重排序,就解決了上面代碼的線程不安全問題
    private static volatile Singleton unsafeObject=null;

synchronized的底層原理:

https://juejin.cn/post/6973571891915128846

  • 對(duì)象鎖:A a=new A(),由于new出的對(duì)象是獨(dú)一無二的,所以只要有線程拿到了相應(yīng)的對(duì)象鎖,導(dǎo)致所有帶有synchronized(a)的方法及代碼塊都會(huì)被鎖住,只有線程釋放了這個(gè)鎖其他的方法或者是代碼塊才能被其他線程執(zhí)行,而synchronized(this)也是個(gè)對(duì)象鎖,this是當(dāng)前對(duì)象鎖,如果A a=new A(),a內(nèi)部有synchronized(this),那么synchronized(a)和synchronized(this)效果是一樣,都是一樣的對(duì)象鎖,所以會(huì)產(chǎn)生同步,有線程使用synchronized(a)進(jìn)入同步代碼塊后,那么其他線程將無法進(jìn)入a這個(gè)實(shí)例對(duì)象內(nèi)部帶有synchronized(this)的代碼塊,除非鎖釋放了

  • 類鎖:synchronized(A.class),和對(duì)象鎖一樣由于類也是獨(dú)一無二的,所以只要有線程拿到了相應(yīng)的類鎖,導(dǎo)致所有帶有synchronized(A.class)的方法及代碼塊都會(huì)被鎖住,只有線程釋放了這個(gè)鎖其他的方法或者是代碼塊才能被其他線程執(zhí)行

  • 方法鎖:非靜態(tài)的方法鎖public void synchronized aaa(),只針對(duì)當(dāng)前對(duì)象的某個(gè)方法,這個(gè)鎖就是當(dāng)前對(duì)象實(shí)例,只要沒有獲得這個(gè)當(dāng)前對(duì)象實(shí)例鎖的線程都不能訪問。靜態(tài)的方法鎖public static void synchronized aaa(),針對(duì)某個(gè)類,這個(gè)鎖就是當(dāng)前這個(gè)對(duì)象的類實(shí)例,只要沒有獲得這個(gè)當(dāng)前對(duì)象類的鎖的線程都不能訪問

synchronized通過對(duì)象頭和monitor實(shí)現(xiàn)的

對(duì)象頭:

synchronized的鎖對(duì)象是存儲(chǔ)在對(duì)象頭里,下面是對(duì)象頭的組成,主要由Mark Word 和Class Metadata Address組成,由于synchronized是重量級(jí)鎖性能差,jvm對(duì)進(jìn)行了優(yōu)化,利用對(duì)象頭實(shí)現(xiàn)了其而對(duì)象頭實(shí)現(xiàn)了輕量級(jí)鎖和偏向鎖
對(duì)象頭
Mark Word

鎖優(yōu)化:

由于synchronized鎖過于重量級(jí),在性能上一直都很差,比ReentrantLock差,HotSpot一直對(duì)鎖進(jìn)行優(yōu)化如:自旋鎖、自適應(yīng)自旋鎖、鎖粗化、鎖消除、輕量級(jí)鎖、偏向鎖等,現(xiàn)在synchronized性能不比ReentrantLock差,并且還在不斷的優(yōu)化中,由于都是系統(tǒng)級(jí)的優(yōu)化,所以synchronized前途是比ReentrantLock光明的

重量級(jí)鎖:

最基礎(chǔ)的實(shí)現(xiàn)方式,JVM會(huì)阻塞未獲取到鎖的線程,在鎖被釋放的時(shí)候喚醒這些線程。阻塞和喚醒操作是依賴操作系統(tǒng)來完成的,所以需要從用戶態(tài)切換到內(nèi)核態(tài),開銷很大。加上monitor調(diào)用的是操作系統(tǒng)底層的互斥量(mutex),而使用操作系統(tǒng)本身也有用戶態(tài)和內(nèi)核態(tài)的切換,又增加了開銷,所以JVM引入了自旋的概念,減少上面說的線程切換的成本。

自旋鎖:

如果鎖被其他線程占用的時(shí)間很短,那么其他獲取鎖的線程只要稍微等一下就好了,主要是使線程不放棄CPU時(shí)間然后執(zhí)行忙循環(huán),沒必要進(jìn)行用戶態(tài)和內(nèi)核態(tài)之間的切換,等的狀態(tài)就叫自旋。在JDK6中是默認(rèn)開啟的通過-XX:-/+UseSpinning設(shè)置關(guān)閉/開啟,通過-XX:PreBlockSpin設(shè)置自旋次數(shù),默認(rèn)是10。在JDK6中引入了自適應(yīng)自旋鎖,使自旋的時(shí)間不在固定,并且可以省略自旋,由同一個(gè)鎖對(duì)象的上次自旋時(shí)間決定是否需要省略本次的自旋操作,如果一個(gè)鎖對(duì)象自旋很少成功,虛擬機(jī)會(huì)直接放棄自旋,相反如果同一鎖對(duì)象自旋經(jīng)常成功虛擬機(jī)會(huì)允許它自旋得久一些。這種機(jī)制讓虛擬機(jī)一定程度上節(jié)省了自旋浪費(fèi)的時(shí)間。

鎖消除:

虛擬機(jī)會(huì)檢測(cè)我們的代碼中不可能出現(xiàn)出現(xiàn)數(shù)據(jù)共享競爭的地方,將多余的鎖進(jìn)行消除

鎖粗化:

如果一系列操作都對(duì)同一個(gè)對(duì)象反復(fù)的加鎖和解鎖,這種現(xiàn)象甚至出現(xiàn)在循環(huán)體中,對(duì)系統(tǒng)的造成不必要的消耗,那么虛擬機(jī)將會(huì)把這個(gè)鎖的范圍擴(kuò)大,把多次加鎖解鎖的地方變成只需要一次加鎖和解鎖

輕量級(jí)鎖:

JDK1.6之后加入,它的目的并不是為了替換前面的重量級(jí)鎖,而是在實(shí)際沒有鎖競爭的情況下,將申請(qǐng)互斥量這步也省掉。鎖實(shí)現(xiàn)的核心在于對(duì)象頭(Mark Word)的結(jié)構(gòu),對(duì)象自身會(huì)有信息表示是否被鎖住和鎖是什么類型的,如果代碼進(jìn)入同步塊時(shí),檢測(cè)到對(duì)象未鎖定,即標(biāo)志位為01。那么當(dāng)前線程就會(huì)在自身?xiàng)薪⒁粋€(gè)名為鎖記錄的區(qū)域拷貝一份對(duì)象的Mark Word信息叫做Displace Mark Word,再使用CAS的方式將對(duì)象Mark Work更改為指向這個(gè)區(qū)域的指針,如果更改成功了就算加上鎖了成功了,那么當(dāng)前線程就算擁有了這個(gè)對(duì)象的鎖了,上鎖成功后這個(gè)對(duì)象的Mark Word的鎖標(biāo)志位將會(huì)變?yōu)?0。(這樣就不需要獲取系統(tǒng)mutex變量,只是改了個(gè)值,但是如果有競爭的話,就要升級(jí)成重量級(jí)鎖,這樣反倒變慢了)

偏向鎖:

偏向鎖將同步操作全部省略,進(jìn)一步的對(duì)鎖進(jìn)行了優(yōu)化,-XX:+UseBiasedLocking可以開啟,它主要實(shí)現(xiàn)是一個(gè)對(duì)象鎖第一次被線程獲取的時(shí)候?qū)ο箢^的標(biāo)志位會(huì)被設(shè)置為“01”表示偏向模式,同時(shí)使用CAS把獲取這個(gè)對(duì)象鎖的線程ID記錄在對(duì)象的Mark Word中,如果CAS成功代表獲取偏向鎖成功,持有這個(gè)偏向鎖的線程每次進(jìn)入這個(gè)對(duì)象鎖相關(guān)的代碼塊時(shí),虛擬機(jī)將不會(huì)對(duì)這個(gè)線程做任何操作,但是如果有其他的線程來嘗試獲取這個(gè)對(duì)象鎖的時(shí)候,偏向模式就會(huì)被撤銷,標(biāo)志位將會(huì)變?yōu)闊o鎖的"01"或者輕量級(jí)鎖的"00",由此可以看出鎖是一個(gè)逐步升級(jí)的過程,不會(huì)一開始上來就重量級(jí)鎖。鎖一般只會(huì)升級(jí)不會(huì)降級(jí),避免降級(jí)之后沖突導(dǎo)致效率不行并且又得升級(jí)。但是降級(jí)其實(shí)是允許的

偏向鎖可以提高帶有同步但無競爭的程序的性能,但是鎖總是會(huì)被多個(gè)線程競爭的,所以偏向模式將會(huì)是多余的,還是禁止掉偏向鎖優(yōu)化比較好。-XX:-UseBiasedLocking禁止

隱式鎖 monitor:

每個(gè)對(duì)象里面隱式的存在一個(gè)叫monitor(對(duì)象監(jiān)視器)的對(duì)象,這個(gè)對(duì)象源碼是采用C++實(shí)現(xiàn)的,monitor內(nèi)部有一個(gè)count變量,調(diào)用monitorenter就是嘗試獲取這個(gè)對(duì)象,成功獲取到了就將值+1,離開就將值-1。如果是線程重入,在將值+1,說明monitor對(duì)象是支持可重入的。如果synchronize在方法上,那就沒有上面兩個(gè)指令,取而代之的是有一個(gè)ACC_SYNCHRONIZED修飾,表示方法加鎖了。它會(huì)在常量池中增加這個(gè)一個(gè)標(biāo)識(shí)符,獲取它的monitor,所以本質(zhì)上是對(duì)象鎖是一樣的。

  • 1、monitorenter: monitorenter指令表示獲取鎖對(duì)象的monitor對(duì)象,這是monitor對(duì)象中的count并會(huì)加+1,如果monitor已經(jīng)被其他線程所獲取,該線程會(huì)被阻塞住,直到count=0,再重新嘗試獲取monitor對(duì)象

  • 2、monitorexit: monitorexit與monitorenter是相對(duì)的指令,表示進(jìn)入和退出。執(zhí)行monitorexit指令表示該線程釋放鎖對(duì)象的monitor對(duì)象,這時(shí)monitor對(duì)象的count便會(huì)-1變成0,其他被阻塞的線程可以重新嘗試獲取鎖對(duì)象的monitor對(duì)象,第二monitorexit是主要作用是發(fā)生異常的時(shí)候可以自動(dòng)釋放鎖

  • 3、synchronized用來修飾方法時(shí)(靜態(tài)方法和非靜態(tài)方法),是通過ACC_SYNCHRONIZED標(biāo)識(shí)符來保持線程同步的。而用來修飾代碼塊時(shí),是通過monitorenter和monitorexit指令來完成,ACC_SYNCHRONIZED修飾時(shí)它同樣是取獲取monitor對(duì)象的鎖,所以本質(zhì)上和對(duì)象鎖一樣

每一個(gè)鎖都對(duì)應(yīng)一個(gè)monitor對(duì)象,在HotSpot虛擬機(jī)中它是由ObjectMonitor實(shí)現(xiàn)的(C++實(shí)現(xiàn))。每個(gè)對(duì)象都存在著一個(gè)monitor與之關(guān)聯(lián),對(duì)象與其monitor之間的關(guān)系有存在多種實(shí)現(xiàn)方式,如monitor可以與對(duì)象一起創(chuàng)建銷毀或當(dāng)線程試圖獲取對(duì)象鎖時(shí)自動(dòng)生成,但當(dāng)一個(gè)monitor被某個(gè)線程持有后,它便處于鎖定狀態(tài)。下圖是ObjectMonitor的C++源碼

ObjectMonitor

ObjectMonitor中有兩個(gè)隊(duì)列_WaitSet和_EntryList,用來保存ObjectWaiter對(duì)象列表(每個(gè)等待鎖的線程都會(huì)被封裝ObjectWaiter對(duì)象),_owner指向持有ObjectMonitor對(duì)象的線程,當(dāng)多個(gè)線程同時(shí)訪問一段同步代碼時(shí),首先會(huì)進(jìn)入_EntryList 集合,當(dāng)線程獲取到對(duì)象的monitor 后進(jìn)入 _Owner 區(qū)域并把monitor中的owner變量設(shè)置為當(dāng)前線程同時(shí)monitor中的計(jì)數(shù)器count加1,若線程調(diào)用 wait() 方法,將釋放當(dāng)前持有的monitor,owner變量恢復(fù)為null,count自減1,同時(shí)該線程進(jìn)入 WaitSe t集合中等待被喚醒。若當(dāng)前線程執(zhí)行完畢也將釋放monitor(鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取monitor(鎖)。
monitor對(duì)象存在于每個(gè)Java對(duì)象的對(duì)象頭中(存儲(chǔ)的指針的指向),synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對(duì)象可以作為鎖的原因,同時(shí)也是notify/notifyAll/wait等方法存在于頂級(jí)對(duì)象Object中的原因(關(guān)于這點(diǎn)稍后還會(huì)進(jìn)行分析)

一個(gè)monitor對(duì)象包括這么幾個(gè)關(guān)鍵字段:_cxq(下圖中的ContentionList),_EntryList ,_WaitSet,_owner。其中_cxq ,_EntryList ,_WaitSet都是由ObjectWaiter組成的鏈表結(jié)構(gòu),_owner指向持有鎖的線程。當(dāng)一個(gè)線程嘗試獲得鎖時(shí),如果該鎖已經(jīng)被占用,則會(huì)將該線程封裝成一個(gè)ObjectWaiter對(duì)象插入到_cxq的隊(duì)列尾部,然后暫停當(dāng)前線程。當(dāng)持有鎖的線程釋放鎖前,會(huì)將cxq中的所有元素移動(dòng)到_EntryList中去,并喚醒_EntryList的隊(duì)首線程。如果一個(gè)線程在同步塊中調(diào)用了wait方法,會(huì)將該線程從_EntryList移除并加入到_WaitSet中,然后釋放鎖。當(dāng)wait的線程被notify之后,會(huì)將對(duì)應(yīng)的線程從_WaitSet移動(dòng)到_EntryList中

image.png

利用javap -verbose 反編譯這段代碼

public class SynchronizedTest {
    public synchronized void methodtest(){
        System.out.println("方法鎖");
    }
    public void objectlock(){
        synchronized (this){
            System.out.println("對(duì)象鎖");
        }
    }
    public void classlock(){
        synchronized (SiSuo.class) {
            System.out.println("類鎖");
        }
    }
    public synchronized static void staticlock(){
     System.out.println("靜態(tài)方法鎖");
    }
    public static void main(String[] args) {
        SynchronizedTest test=new SynchronizedTest();
        test.methodtest();
        test.objectlock();
        test.classlock();
        SynchronizedTest.staticlock();
    }
}

image.png
image.png
image.png
image.png

線程的狀態(tài):

由上面分析的monitor知道線程在獲取鎖時(shí)會(huì)被封裝成monitor進(jìn)入ContentionList,EntryList ,WaitSet,owner幾個(gè)隊(duì)列代表著線程的不同狀態(tài)
image.png

線程中共有六種狀態(tài):

  • 1、新建(New):被創(chuàng)建(new)出來,但是沒有啟動(dòng)的線程

  • 2、運(yùn)行(Runnable):運(yùn)行狀態(tài)又分為兩種狀態(tài):就緒和運(yùn)行。線程被創(chuàng)建,并行執(zhí)行start后,線程進(jìn)入可運(yùn)行線程池中,等待CPU執(zhí)行時(shí)間,此時(shí)是就緒狀態(tài)。線程就緒狀態(tài)獲取CPU執(zhí)行時(shí)間后進(jìn)入運(yùn)行狀態(tài)

  • 3、無限期等待(Waiting):此狀態(tài)線程不會(huì)獲得CPU執(zhí)行時(shí)間,除非被主動(dòng)喚醒

  • 4、限期等待(Time Waiting):此狀態(tài)的線程不會(huì)獲得CPU執(zhí)行時(shí)間,直至被系統(tǒng)喚醒。設(shè)置了Thread.sleep();

  • 5、阻塞(Blocked):線程等待獲取排它鎖

  • 6、結(jié)束(Terminated):線程已經(jīng)終止,線程執(zhí)行結(jié)束。在一個(gè)終止的線程執(zhí)行操作如執(zhí)行:t.start(),會(huì)拋出java.lang.IllegalThreadStateException

線程的這些狀態(tài)可以通過sleep、wait、notify、notifyAll、yield、interrupt進(jìn)行轉(zhuǎn)換

sleep不必多說,平時(shí)我們用過,它可以使線程進(jìn)入期限等待中,sleep狀態(tài)下的線程只會(huì)讓出CPU執(zhí)行時(shí)間,不會(huì)讓出鎖

interrupt:提醒系統(tǒng)線程應(yīng)該被終止,如果線程處于阻塞狀態(tài),線程會(huì)退出阻塞如Thread.sleep();,拋出異常。如果線程是正?;顒?dòng)的,將會(huì)把線程的中斷標(biāo)志設(shè)置為true,然后線程正常運(yùn)行不受影響.interrupt中斷僅僅是設(shè)置中斷標(biāo)記位。對(duì)于NEW|TERMINATED 線程,終端完全不起作用;對(duì)于RUNNABLE或者BLOCKED線程,只會(huì)將中斷標(biāo)志位設(shè)為true;WAITING線程對(duì)中斷較為敏感,會(huì)拋出異常,同時(shí)會(huì)將中斷標(biāo)志位清除變?yōu)閒alse。

public void interrupt(): 設(shè)置當(dāng)前線程的中斷標(biāo)志位,非WAITING線程不會(huì)受任何影響的,僅此而已。
public boolean isInterrupted(): 判斷當(dāng)前線程中斷標(biāo)志位是否被標(biāo)記為中斷,不會(huì)清除標(biāo)志位
public static boolean interrupted():這是一個(gè)靜態(tài)方法,返回當(dāng)前線程是否標(biāo)記中斷,同時(shí)清除標(biāo)志位

wait、notify、notifyAll:
wait:線程執(zhí)行wait,讓線程進(jìn)入無限期等待狀態(tài),只有其他線程執(zhí)行notify或者notifyall才被喚醒,wait只能在synchronized的代碼塊或在synchronized類中使用,它會(huì)讓線程不但需要讓出CPU執(zhí)行時(shí)間,還必須讓出鎖給其他線程

wait被執(zhí)行后,線程失去CPU時(shí)間和鎖,線程被扔進(jìn)等待池,此線程在等待池中不會(huì)去競爭鎖,直至有其他線程對(duì)鎖執(zhí)行notify或者notifyall,線程才會(huì)被扔進(jìn)鎖池中去競爭鎖,notify和notifyall的區(qū)別在于,notify只會(huì)隨機(jī)喚醒一個(gè)wait()的線程,而notifyall會(huì)喚醒所有wait()的線程

yield:線程暗示CPU,愿意讓出CPU時(shí)間給其他線程,但是CPU不一定會(huì)理會(huì)這個(gè)暗示,最終的決定線程是否讓出CPU時(shí)間由CPU決定

此外還有一個(gè)join方法,代表著“插隊(duì)”,哪個(gè)線程調(diào)用join代表哪個(gè)線程插隊(duì)先執(zhí)行——但是插誰的隊(duì)是有講究了,不是說你可以插到隊(duì)頭去做第一個(gè)吃螃蟹的人,而是插到在當(dāng)前運(yùn)行線程的前面,比如系統(tǒng)目前運(yùn)行線程A,在線程A里面調(diào)用了線程B.join方法,則接下來線程B會(huì)搶先在線程A面前執(zhí)行,等到線程B全部執(zhí)行完后才繼續(xù)執(zhí)行線程A。join內(nèi)部是調(diào)用了wait方法對(duì)目前正在運(yùn)行的線程進(jìn)行阻塞,注意這里線程執(zhí)行后是調(diào)用notifyAll進(jìn)行喚醒的,也就是B調(diào)用notifyAll喚醒A

安全發(fā)布對(duì)象:

  • 1、在靜態(tài)初始化函數(shù)中初始化一個(gè)對(duì)象引用
  • 2、將對(duì)象的引用保存到volatile類型域或者AtomicReference對(duì)象中
  • 3、將對(duì)象的引用保存到某個(gè)正確構(gòu)造的final類型域中
  • 4、將對(duì)象的引用保存到一個(gè)由鎖保護(hù)的域中
單例模式安全發(fā)布演示
//線程安全
//懶漢
public class Singleton {
 private Singleton(){}
//使用volatile禁止指令重排
    private static volatile Singleton unsafeObject=null;
    public static Singleton unsafeObject(){
        if (unsafeObject==null){
            synchronized (Singleton.class){
                if (unsafeObject==null) {
                    return new Singleton();
                }
            }
        }
        return unsafeObject;
    }
}
//線程安全
//餓漢
public class Singleton {
 private Singleton(){}
//使用volatile禁止指令重排
    private static Singleton unsafeObject=new Singleton();
    public static Singleton unsafeObject(){
        return unsafeObject;
    }
}
//枚舉方式線程安全
public class SingletonEunm {
    private SingletonEunm(){}
    //枚舉類相比餓漢模式更加節(jié)省資源,原因是枚舉調(diào)用的時(shí)候才創(chuàng)建,并且只創(chuàng)建一次,不像靜態(tài)代碼在項(xiàng)目啟動(dòng)時(shí)提前創(chuàng)建好,資源浪費(fèi)
    private static SingletonEunm getSingletonEunm(){
        return InstanceSingleton.INSTANCE.getSingleton();
    }
    private enum InstanceSingleton{
        INSTANCE;
        private SingletonEunm singletonEunm;
        //對(duì)于枚舉類JVM保證只初始化一次,并且是調(diào)用的時(shí)候才初始化
        InstanceSingleton(){
            singletonEunm=new SingletonEunm();
        }
        public SingletonEunm getSingleton(){
            return singletonEunm;
        }
    }
}
使用final安全發(fā)布演示
public class Final {
    private static final int SAFEINT=2333;//這個(gè)基本類型不可修改,安全
//SAFESTR這個(gè)引用不可在指向其他對(duì)象,“2333”字面量也是對(duì)象,所以SAFESTR這個(gè)引用無法指向其他對(duì)象也就是無法修改它的字面量了,安全
    private static final String SAFESTR="2333";
//ALLOW這個(gè)引用無法指向其他的對(duì)象了,但是可以修改指向?qū)ο蟮膬?nèi)容
   private static final Map<String,String> ALLOWMAP= new HashMap();
 private static Map<String,String> UNMAP= new HashMap();
//利用Gava使GUNMAP內(nèi)容不可變,final 使GUNMAP引用不可指向其他對(duì)象
 private static final Map GUNMAP= ImmutableMap.of("k1","v1","k2","v2");
    private static final List GUNLIST= ImmutableList.of("111","222");
    private static final Set GUNSET= ImmutableSet.of("111","222");
 static {
        UNMAP.put("111","222");
 //使用Collections.unmodifiableMap使Map里的內(nèi)容不可變
        UNMAP= Collections.unmodifiableMap(UNMAP);
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容