主頁 > 軟體設計 > 并發編程系列——wait原理的討論(1)

并發編程系列——wait原理的討論(1)

2020-12-20 12:25:27 軟體設計

失蹤人口回歸

好久沒來寫博客了,謝謝催更的網友你們的催更便是我寫作的動力;但是這篇不想寫spring了,寫關于并發編程的,實用性比較大,特別對于面試;這篇博客來討論關于synchronized關鍵字的wait方法的原理;筆者感觸比較大,如果你想學好并發編程務必好好看看這篇文章,好了正篇開始,源于一個網友的問題如下

在這里插入圖片描述

總結如下:synchronized關鍵字的呼叫wait方法進入等到的執行緒和因為拿不到鎖而等待執行緒是否同一種狀態?blocking?waiting?

別小看這個問題,要扯清這個問題需要大篇幅的文字,所以再次長文警告;而且筆者可以很負責任的告訴讀者如果你能看懂這篇文章絕對會燃起你對并發編程學習的興趣

關于blocking狀態的執行緒

synchronized關鍵字的blocking

在多執行緒編程的情況下,假設我們定義了一把鎖,如果現在有10個執行緒來獲取這把鎖那么肯定只有第一個執行緒可以獲取到鎖,從而進入臨界區(所謂臨界區就是被鎖保護起來的代碼塊);其他獲取不到鎖的執行緒都會被阻塞(關于阻塞你就可以理解為CPU放棄調度這個執行緒了),但是這些被阻塞的執行緒JVM是怎么處理的呢?先看一張圖
在這里插入圖片描述
上圖t1獲取鎖,如果在t1沒有釋放的情況下其他執行緒也來獲取鎖,結果肯定是獲取不到,從而進入阻塞狀態,但是這些被阻塞的執行緒如果不存某種關系將來喚醒的時候就很麻煩(比如先喚醒誰呢?有人肯定會說那肯定先喚醒最先阻塞的那個執行緒啊,關鍵是JVM如何知道哪個執行緒最先阻塞的呢?)為了解決這個麻煩JVM設計了一個EntryList的雙向鏈表的佇列來維護這些阻塞的執行緒;如上圖這樣 t2到tn被維護到了這個佇列,當t1釋放鎖之后會去這個佇列當中喚醒一個執行緒來獲取鎖,這里請讀者們思考一些問題;到底是喚醒一個,還是全部喚醒呢?如果喚醒一個是隨機喚醒還是順序喚醒,如果是順序喚醒是正序還是倒序呢?筆者直接給出答案,當t1釋放鎖的時候會從EntryList當中喚醒一個執行緒,而且順序喚醒,而且倒序的,也就是先喚醒tn這個執行緒;但是值得注意的synchronized關鍵字是倒序喚醒,但是如果你使用ReentrantLock那么則是正序喚醒;那么這個結論如何證明了----筆者將會通過三個角度來證明
1、通過一個簡單java應用來證明

2、是通過JDK內部關于ReentrantLock鎖的實作來證明;(因為ReentrantLock和synchronized關鍵字都是實作同步鎖,他們都有這么一個佇列,原理差不多,其實就算我能通過ReentrantLock來證明這個雙向串列的佇列真實存在也不能說明synchronized關鍵字也有這么一個佇列啊,確實是這樣,但是由于ReentrantLock的這個佇列是java語言實作的,比較容易看懂,畢竟是母語啊,所以先看懂java語言級別的實作——synchronized關鍵字是沒有java級別原始碼可看的,他是通過C++代碼來實作的,先看懂java的實作再來看C++會輕松點)

3、如果有可能我會通過JVM原始碼中來證明這個佇列的存在

首先來看一個簡單的java應用,代碼如下(先仔細閱讀以下代碼,下文我會對代碼做解釋,博客里面所有代碼放到文末鏈接,讀者可以自己下載);

package com.shadow.test;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Slf4j(topic = "shadow")
public class TestSynchronized {
    static List<Thread> list = new ArrayList<>();
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(() -> {
                synchronized (lock) {
                    log.debug("thread executed");
                    try {
                        //這里的睡眠沒有什么意義,僅僅為了控制臺列印的時候有個間隔 視覺效果好
                        TimeUnit.MILLISECONDS.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "t" + i);//給每個執行緒去了一個名字 t1 t2 t3 ....

            list.add(t);
        }

        log.debug("---啟動順序 調度順序或者說獲取鎖的順序講道理是正序0--9----");
     
        synchronized (lock) {
            for (Thread thread : list) {
                //這個列印主要是為了看到執行緒啟動的順序
                log.debug("{}-啟動順序--正序0-9", thread.getName());
                thread.start();//  CPU 調度?
                
                //這個睡眠相當重要,如果沒有這個睡眠會有很大問題
                //這里是因為執行緒的start僅僅是告訴CPU執行緒可以調度了,但是會不會立馬調度是不確定的
                //如果這里不睡眠 就有有這種情況出現
                // 主執行緒執行t1.start--Cpu沒有調度t1--繼續執行主執行緒t2-start cpu調度t2--然后再調度t1
                //雖然我們的啟動順序是正序的(t1--t2),但是調度順序是錯亂的  t2---t1
                
                TimeUnit.MILLISECONDS.sleep(1);
             }
             log.debug("-------執行順序--正序9-0");
        }
    }
}

代碼非常簡單主執行緒main啟動,然后實體化了10個執行緒物件t0-t9;繼而把這個10個執行緒添加到一個List當中(注意這里僅僅是實體化了十(10)個執行緒,并沒有啟動,如果將來啟動這10個執行緒他們的run方法里面的代碼也非常簡單,就是獲取lock這把鎖,然后列印一句話);添加到陣列之后main執行緒接著往下執行;mian執行緒獲取鎖(這里一定能獲取成功,因為那10個執行緒還沒啟動,鎖處于自由狀態,所以能被main獲取);獲取到鎖之后main執行緒執行了一個for回圈從list當中依次順序獲取到上面存入到list當中的那10個執行緒(由于ArrayList是有序的)故而取出的順序肯定是有序的(t0-t9);取出來之后依次呼叫star方法啟動這些執行緒;但是這里需要注意的是雖然我們已經保證取出來的執行緒是順序的(t0-t9),而且我們也保證了這些執行緒的start方法是順序呼叫的,但是你依然沒法保證這些執行緒的調度(也就是我們常說的執行)順序;為了保證t0-t9的調度順序我這里在執行緒start之后,讓main執行緒sleep了1毫秒;這樣就能保證t0-t9執行緒的調度或者說執行順序;至于為什么要保證他們的調度順序?

  			 來解釋一下為什么需要保證這個調度順序呢?
             這里所有代碼的意圖就是順序啟動執行緒(順序調度執行緒),這些執行緒啟動之后會去拿鎖(lock)
             肯定拿不到,因為這個時候鎖被主執行緒持有
             主執行緒還在for回圈沒有釋放鎖,所以在for回圈里面啟動的執行緒都是拿不到鎖的
             那么這些那不到鎖的執行緒就會阻塞
             也就t0----t9陽塞之后他們被存到了一個佇列當中
             這個JVM的原始碼中可以證明,我后面給大家看原始碼,
             總之你現在記住所有拿不到鎖的執行緒會阻塞進入到Entrylist這個佇列當中
             然后主執行緒執行完for回圈后會釋放放鎖
             繼而會去這個佇列當中去喚醒一個個執行緒————隨機喚醒還是順序喚醒呢?
             假設是順序喚醒,是倒序還是正序喚醒呢?
             需要證明這個問題,就要保證所有因為拿不到鎖而進入到這個佇列當中的執行緒
             他們的順序必須是有序的,這樣后面從他們的執行結果才能分析;
             假設你 進入到阻塞佇列的時候都是隨機的,那么后面喚醒執行緒執行的時候必然也是隨機的
             那么則無法證明喚醒是否具備有序性
             為了保證進入到佇列當中的執行緒調度是有序的,主執行緒睡眠很有必要  
             那么為什么主執行緒睡眠1下就能保證這些執行緒的順序調度呢?這個問題讀者可以思考一下后而我會重點分析
             好了現在我們來看結果

在這里插入圖片描述
從上圖可以看出首先10個執行緒的啟動順序(由于主執行緒睡眠了1毫秒故而啟動順序其實等于調度順序)是t0-t9;因為在啟動執行緒的時候主執行緒沒有釋放鎖,所以t0-t9都因為拿不到鎖進入了佇列(EntryList),又因為t0-t9的調度(啟動)順序保證了,所以進入佇列的順序也保證了(t0先進入佇列,t9最后進入佇列);但是在主執行緒釋放鎖的時候,喚醒執行緒的順序是都倒序的,先喚醒t9,最后喚醒t0;這里的結果可以說明JVM在從佇列當中喚醒的時候是喚醒一個,而不是全部喚醒,因為如果是全部喚醒,那么這些執行緒的執行順序肯定是亂的,只有喚醒一個,而且還是順序喚醒才能保證執行順序是具備規則的(t9-t0),而且是倒序喚醒的;那么這佇列存在哪里呢?在java語言里使用synchronized關鍵字如果變成了一把重量鎖(關于什么是重量鎖下次分析),那么這個鎖物件(本文當中的lock物件——Object lock = new Object())會關聯一個C++物件——ObjectMonitor物件;這個監視器物件當中記錄了持有當前鎖的執行緒,記錄了鎖被重入的次數,同時他還有一個屬性EntryList用來關聯那些因為拿不到鎖而被阻塞的執行緒;如下圖所示(先不要關心WaitSet)

在這里插入圖片描述
好了到此為止你應該明白了我們在程式當中使用synchronized關鍵字的大概原理了吧,總結一下,當我們使用synchronized關鍵字來保護臨界區的代碼的時候,如果多執行緒并發情況下,持有鎖的執行緒只有一個;其他競爭鎖而不得執行緒會進入到當前鎖物件關聯的ObjectMonitor物件當中的雙向鏈表的一個佇列當中,并且是順序進入的;但是當持有鎖執行緒釋放鎖之后會從這個佇列當中喚醒一個執行緒;從佇列的末尾喚醒一個(先進后出);如果你沒有看懂,請務必把文章翻上去在去看一遍或者從文章末尾把代碼下載過去自己運行一遍,然后好好體會一下;因為只有把這里搞懂了下面筆者要寫的內容才有意義,否則很難看懂下面的文章;

ReentrantLock的Blocking

這次代碼改一下,不用synchronized關鍵來保護臨界區,而是換成ReentrantLock,代碼如下

package com.shadow.test;


import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j(topic = "s")
public class TestReentrantLock {
    
    static List<Thread> list = new ArrayList<>();
    //代碼都沒有變,只是把synchronized關鍵字變成了ReentrantLock
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(() -> {
                lock.lock();
                    log.debug("thread executed");
                    try {
                        TimeUnit.MILLISECONDS.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
               lock.unlock();
            }, "t" + i);
            list.add(t);
        }
        log.debug("---啟動順序 調度順序或者說獲取鎖的順序講道理是正序0--9----");
        lock.lock();
            for (Thread thread : list) {
                log.debug("{}-啟動順序--正序0-9", thread.getName());
                thread.start();//  CPU 調度?
                TimeUnit.MILLISECONDS.sleep(1);
            }

            log.debug("-------執行順序--正序9-0");

        lock.unlock();
    }
}

看看運行結果

在這里插入圖片描述

從結果可以看到使用ReentrantLock來保護臨界區的時候效果幾乎和synchronized關鍵字相同,唯一不同的是當主執行緒釋放鎖之后去EntryList(EntryList其實是C++的佇列,ReentrantLock其實不存在EntryList這個佇列,但是他有一個物件FairSync或者NonfairSync這個物件維護了一個佇列類似EntryList,文中為了方便都稱之為EntryList吧)當中喚醒執行緒的時候是正序的(先進先出從);由于ReentrantLock是用java語言實作的,可以通過查閱JDK原始碼來看看他的原理;synchronized需要查詢JVM的C++原始碼;如果大家對并發編程感興趣可以給我留言,我會持續更新,看有么有機會來寫一篇關于synchronized關鍵的底層C++代碼實作;好了現在我們來翻翻JDK原始碼中對ReentrantLock的原始碼實作吧(其實,筆者早先寫過一篇關于ReentrantLock的原始碼實作,而且你可以去b站search關鍵字"子路 AQS"有視頻版的AQS原始碼分析),這里不做特別細致的原始碼分析,只做簡單的原始碼分析;

首先看一下Node類設計,由于Node類是AbstractQueuedSynchronizer的一個內部類,我沒有截取到類名,只截取到父類的名字(JDK原始碼中Node主要用來封裝執行緒,因為node類里面有一個屬性就是Thread型別的,你可以理解一個node物件就是一個執行緒)

在這里插入圖片描述
主要關心三個屬性

Node  prev    雙向串列用來指向上一個node(也就是上一個執行緒)
Node  next    雙向鏈表用來指向下一個node(也就是下一個執行緒)
Thread thread  當前node所封裝的執行緒

如果單純看這三個屬性,可以理解Node類主要是為了執行緒之間有關聯而設計的,因為如果沒有這個Node那么單獨一個Thread是很難描述清楚執行緒之間的關系的;比如上面代碼中t0-t9他們的阻塞順序靠一個Thread是很難表示的;有了這個Node就很好表示了,比如node0物件當中的thread=t0,prev=null;next=node1;而node1當中的thread=t1,prev=node0,next=node2…以此類推吧(實際當中Node類有很多屬性的,這里不做討論);

再來看AQS這個同步框架最核心的類的設計;同樣我們只關心他的幾個屬性

在這里插入圖片描述

Node head	//佇列當中的對頭,也就是第一個阻塞的執行緒封裝出來的node物件
Node tail	//佇列當中的對尾,也就是最后一個阻塞的執行緒封裝出來的node物件
int state	//鎖的重入次數

如果我們使用ReentrantLock來保護臨界區當一個執行緒拿不到鎖的時候,會把這個執行緒封裝成為一個Node物件;比如當t7來獲取鎖的時候則持有鎖的執行緒是main,重入次數為1,對頭為t0,隊尾為t6;自己被封裝成為一個node物件(此時還沒有進入佇列,也就是還沒有關聯之前)如下圖(介于圖片大小問題中間的Node忽略了,比如t2所代表的node,t3所代表的node)

在這里插入圖片描述
當封裝好t7之后,這個時候t7所代表的node會進入到佇列,進入佇列之后如下圖(介于圖片大小問題中間的Node忽略了,比如t2所代表的node,t3所代表的node)

在這里插入圖片描述
接下來通過idea當中的debug來說明一下上面的理論是否正確

在這里插入圖片描述

上圖的debug程序對于新手來說比較晦澀,可以多看幾遍,或者文章末尾拿到筆者的代碼自己去除錯;主要需要說明的佇列當中的對頭關聯并不是t0,而是一個thread=null,這個讀者可以忽略(其實我在另一篇博客里面解釋過了),也是就結果和我上面講的有一些偏差,但是不影響,因為要解釋這個thread=null代價比較大,讀者可以把圖換一下就一模一樣了(實際情況如下圖)
在這里插入圖片描述
ReentrantLock當中的這個AQS雙向鏈表佇列相當于synchronized關鍵字當中的那個EntryList雙向鏈表佇列;只不過ReentrantLock這個佇列是先進先出,而EntryList則相反是先進后出;這個上面已經通過例子證明了;

其實ReentrantLock的先進先出可以通過原始碼來說明的;我們可以看看他的解鎖方法也就是unlock方法看他如何喚醒執行緒的就真相大白了;

在這里插入圖片描述

好了說了這么多最后給大家總結一下

不管是synchronized還是使用ReentrantLock來做同步,并發情況下
所有拿不到鎖的執行緒都會進入一個雙向鏈表去阻塞
而進入這個佇列當中阻塞的執行緒的狀態就是blocking狀態
至于什么是waiting狀態呢?
同樣我會通過synchronized和ReentrantLock兩個技術點來說明

waitingg狀態

首先我們假設這樣一個場景jack是您們公司的一名程式員,他由于經常看筆者的博客;故而水平非常的高,經常能解決一些核心問題,所以逼格也很高;而各位讀者就是程式員x,水平比較低,幾乎沒有逼格;假設你們老板在周末休息時間打電話叫所有程式員來加班,公司鑰匙只有一把(進入到公司的人會把門鎖了),所以能進到公司一定需要這把鑰匙;這個時候jack來了,但是前面說過jack逼格很高他加班必須要你們老板給他安排一個女人,他才會啪啪啪(當然這里的啪啪啪是指敲鍵盤),不然他就會去休息,而你們是沒有逼格的,進到公司就寫代碼;(有么有一點感同身受啊),基于這個場景我們來編程

package com.shadow.test;


import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "s")
public class TestWait1 {

    static  Object object = new Object();//鎖物件
    static boolean isWoman = false; // 是否有女人

    public static void main(String[] args) {
        new Thread(() -> {//jack
            synchronized (object){
                while (!isWoman){//判斷是否有女人
                    log.debug("沒有女人 我去等待老板安排 先休息,安排好之后叫醒我");
                    try {
                        TimeUnit.SECONDS.sleep(1000000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("開始作業啪啪啪啪");
            }
        }, "jack").start();


        for (int i = 0; i <5 ; i++) {
            new Thread(() -> {
                synchronized (object){
                    log.debug("那些默默無聞的程式員coding");
                }
            }, "程式員"+i).start();
        }
    }

}

代碼其實很簡單,就是jack先獲取到鎖,然后發覺沒有女人,不能啪啪啪(再次強調這里的啪啪啪指的是敲鍵盤);然后他就去休息了;然后其他彩筆程式員(for i=5)由于獲取不到鎖而不能作業無法啟動;結果如下圖

在這里插入圖片描述

這樣顯然不合理,因為jack的女人問題,搞得其他五個人無法作業,可能被fire,這像極了我們平時,老板叫加班不敢不去,如果因為jack去不成那基本要被人事約談,所以不合理;不合理的地方在于jack呼叫了sleep去阻塞,sleep阻塞的執行緒是無法釋放鎖的;假設有一種API能夠讓執行緒阻塞,同時又把鎖釋放了那就最好,jack他牛逼他去休息等女人,不影響其他人受虐心甘情愿的加班,JDK當中對于synchronized關鍵提供了一個wait方法可以實作上述功能

把代碼修改一下,把sleep改成wait

package com.shadow.test;


import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "s")
public class TestWait1 {

    static  Object object = new Object();//鎖物件
    static boolean isWoman = false; // 是否有女人

    public static void main(String[] args) {
        new Thread(() -> {//jack
            synchronized (object){
                while (!isWoman){//判斷是否有女人
                    log.debug("沒有女人 我去等待老板安排 先休息,安排好之后叫醒我");
                    try {
                        
                        //jack執行緒進入阻塞,但是釋放了鎖
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("開始作業啪啪啪啪");
            }
        }, "jack").start();


        for (int i = 0; i <5 ; i++) {
            new Thread(() -> {
                synchronized (object){
                    log.debug("那些默默無聞的程式員coding");
                }
            }, "程式員"+i).start();
        }
    }

}

再次運行,其他五個人可以作業了,而jack則在等待女人(JVM沒有退出,因為jack執行緒阻塞了)
在這里插入圖片描述

但是五個彩筆只能寫CRUD關鍵高并發的核心代碼還是得jack來啊,如果他休息專案基本要黃;故而老板沒有辦法只能滿足他——找個女人來,找個橋本有菜來給jack(這就是大神和你的區別吧可能);難么找來之后怎么喚醒jack呢?jdk當中提供了notify/notifyall來喚醒因為wait方法而被阻塞的執行緒

把代碼再次改一下,添加一個boos執行緒,來滿足jack的條件讓isWoman=true,然后呼叫notifyAll來喚醒jack,叫醒之后jack啪啪啪完之后全部執行緒結束,JVM退出

package com.shadow.test;


import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "s")
public class TestWait1 {

    static  Object object = new Object();//鎖物件
    static boolean isWoman = false; // 是否有女人

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {//jack
            synchronized (object){
                while (!isWoman){//判斷是否有女人
                    log.debug("沒有女人 我去等待老板安排 先休息,安排好之后叫醒我");
                    try {

                        //jack執行緒進入阻塞,但是釋放了鎖
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("開始作業啪啪啪啪");
            }
        }, "jack").start();


        for (int i = 0; i <5 ; i++) {
            new Thread(() -> {
                synchronized (object){
                    log.debug("那些默默無聞的程式員coding");
                }
            }, "程式員"+i).start();
        }



        //這里睡眠主要是為了視覺效果,沒什么意義
        //5s之后叫醒jack
        TimeUnit.SECONDS.sleep(5);
        new Thread(() -> {//jack
            synchronized (object){
               isWoman=true;
               log.debug("jack 橋本有菜來了,你可以啪啪啪了");
               object.notifyAll();
            }
        }, "boss").start();

    }
}

運行結果如下

在這里插入圖片描述

現在隨著你們專案的推進代碼越來越多,jack腎再好也啪不動了,故而你們老板再請了一個同樣經常看筆者博客的大神——女程式員rose;rose由于經常給我的博客點贊水平比jack還牛逼,自然逼格更高,他如果加班則需要加班費,否則也是不會干活去休息,于是再改下代碼(為了簡單我把另外五個執行緒的代碼注釋了)

package com.shadow.test;


import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "s")
public class TestWait1 {

    static  Object object = new Object();//鎖物件
    static boolean isWoman = false; // 是否有女人
    static boolean isMoney = false; // 是否加錢了

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {//jack
            synchronized (object){
                while (!isWoman){//判斷是否有女人
                    log.debug("沒有女人 我去等待老板安排 先休息,安排好之后叫醒我");
                    try {

                        //jack執行緒進入阻塞,但是釋放了鎖
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("開始作業啪啪啪啪");
            }
        }, "jack").start();




        new Thread(() -> {
            synchronized (object){
                while (!isMoney){
                    log.debug("沒有加錢,先休息不干活");
                    try {
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("干活了----有錢能使鬼推");
            }
        }, "rose").start();


//        for (int i = 0; i <5 ; i++) {
//            new Thread(() -> {
//                synchronized (object){
//                    log.debug("那些默默無聞的程式員coding");
//                }
//            }, "程式員"+i).start();
//        }



        //這里睡眠主要是為了視覺效果,沒什么意義
        //5s之后叫醒jack
        TimeUnit.SECONDS.sleep(5);
        new Thread(() -> {//jack
            synchronized (object){
               isWoman=true;
               log.debug("jack 橋本有菜來了,你可以啪啪啪了");
               object.notifyAll();
            }
        }, "boss").start();

    }

}

上面代碼你們老板并沒有加錢只是找了女人,但是呼叫了notifyAll把所有因為wait方法而阻塞的執行緒全部叫醒了,但是只有jack會作業——因為滿足了女人條件,但是沒有滿足加錢條件所以rose還是不會作業會一直阻塞——JVM不會退出;
在這里插入圖片描述

當然老板如果把isMoney=true那么rose也會干活,并且干活完成之后JVM也會正常結束,這個筆者就不在演示了;
這里讀者可以思考一個問題,因為老板呼叫的是notifyAll故而把所有執行緒叫醒了(jack和rose),但是如果老板呼叫的是notify(只會叫醒一個),那么老板叫醒的是誰呢?假設叫醒的是jack還好說,因為本身只滿足了女人的條件叫醒他不為過,但是如果叫醒一個——剛好叫醒的是rose,而rose發現條件不滿足繼續wait,此時jack也在wait這樣就不合理了,明明老板找來了橋本有菜,可是因為叫醒的人不對從而導致沒有人作業,這下老板不是虧大了?(據我所知橋本有菜很貴的);那么notify的叫醒規則是什么呢?關于這個問題筆者下次更新吧;其實這也是synchronized關鍵字wait的一個缺陷;在ReentrantLock當中這個問題得到了完美解決;ReentrantLock當中也提供了類似wait的功能叫做Condition,和wait不同是Condition可以有多個休息室,比如因為女人而wait的jack可以進入A休息室,而因為加錢而wait的rose可以進入B休息室(當然你也可以讓rose也進入A休息室);這樣當條件滿足的時候可以根據條件叫醒不同休息室的人;synchronized的wait方法是讓執行緒進入一個佇列WaitSet(休息室),不管你是因為女人還是因為加錢——jack和rose都進入WaitSet佇列(同一個休息室,因為synchronized只有一個休息室);

在這里插入圖片描述

因為拿不到鎖而阻塞的執行緒(t0-t9)會進入EntryList阻塞;因為某個條件不滿足被wait之后的執行緒進入WaitSet佇列阻塞;那么為什么JVM不讓wait的執行緒直接進入到EntryList當中呢?而是還要設計出來一個WaitSet的佇列出來專門存放wait的執行緒呢?
其實主要是因為如果都存放在EntryList當中那么JVM是無法區分這些執行緒是為什么而被阻塞的;而且將來喚醒的時候也無法確定該不該喚醒;試想這么一個場景,t0持有鎖,t1是因為拿不到鎖而阻塞進入EntryList當中阻塞,t2早先持有鎖,因為某個條件不滿足呼叫了wait也進入到EntryList當中阻塞(這里我們假設JVM把所有阻塞的執行緒都放到EntryList當中);那么當t0釋放鎖之后講道理他應該自動喚醒一個線程的,假設現在他喚醒的是t2就不合理啊,因為t2是wait而阻塞的不能自動喚醒,需要notify喚醒;所以JVM需要再設計一個waitSet來存放因為條件不滿足呼叫了wait而阻塞的執行緒;

那么所有存在waitSet當中的執行緒狀態就稱之為waiting狀態;現在如果你搞清楚了waiting和blocking那么接下來還有一個比較難的問題;所有waiting狀態的執行緒必須經過blocking狀態才能進入runing狀態怎么理解?也就是一個waiting狀態的執行緒是無法直接運行的,需要從waiting轉換成blocking狀態才能運行;說白了就是你呼叫notifyAll其實是把waitset當中所有阻塞執行緒轉移到EntryList當中,然后當持有鎖的執行緒釋放鎖之后再去EntryList當中的末尾(synchronized是末尾,ReentrantLock是最前面)喚醒一個執行;畫幅圖說明一下

現在持有鎖的是main,假設t0-t9是blocking,jack和rose是waiting則如下圖所示

在這里插入圖片描述

如果現在主執行緒呼叫notifyAll方法會喚醒jack和rose,這里的喚醒其實是把它們轉移到EntryList末尾,如下圖所示
在這里插入圖片描述
那么這個究竟是我吹牛逼還是真的如此呢?我通過一個例子和原始碼來證明
首先看一個簡單的java例子

package com.shadow.test;
import lombok.extern.slf4j.Slf4j;
import org.omg.CORBA.TIMEOUT;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "s")
public class TestWait2 {

    static  Object object = new Object();//鎖物件
    static boolean isWoman = false; // 是否有女人

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {//jack
            synchronized (object){
                while (!isWoman){//判斷是否有女人
                    log.debug("沒有女人 我去等待老板安排 先休息,安排好之后叫醒我");
                    try {
                        //jack執行緒進入阻塞,但是釋放了鎖
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("開始作業啪啪啪啪");
            }
        }, "jack").start();


        TimeUnit.SECONDS.sleep(1);

        log.debug("1s之后主執行緒獲取鎖");
        log.debug("因為jack wait釋放了鎖,主執行緒能夠獲取到");
         log.debug("-----------關鍵看列印順序----------------");
        synchronized (object) {
           
            for (int i = 0; i < 5; i++) {
                new Thread(() -> {
                    synchronized (object) {
                        log.debug("那些默默無聞的程式員coding");
                    }
                }, "t" + i).start();
                TimeUnit.MILLISECONDS.sleep(1);
            }

            isWoman=true;
            object.notifyAll();
        }


    }

}

解釋一下代碼,首先jack獲取到了鎖,然后呼叫了wait方法進入了waitSet阻塞,跟著主執行緒獲取了鎖,主執行緒獲取鎖之后啟動了5個執行緒(t0-t4);這5個執行緒也要獲取鎖,但是因為現在鎖被主執行緒持有所以這五個執行緒肯定是獲取不到鎖的,繼而進入EntryList阻塞;啟動完5個執行緒之后主執行緒滿足條件isWoman=true;從而喚醒jack如果jack是即時喚醒執行的話,那么列印的順序肯定是jack先執行(誠然結果也是jack先執行,但是其實這并不能說明notifyAll是即時喚醒),然后是t4-t0執行;但是筆者講過notifyAll其實是轉移執行緒到EntryList,也就是說當主執行緒呼叫完object.notifyAll();之后EntryList當中應該存在6個執行緒 t0、t1、t2、t3、t4、jack,如下圖所示

在沒有呼叫notifyAll之前

在這里插入圖片描述

呼叫完notifyAll之后

在這里插入圖片描述
主執行緒在呼叫完notifyAll之后就釋放了鎖,在這個時候JVM會去EntryList當中的末尾喚醒一個執行緒就是jack(這個上面我們已經證明過了——倒序);所以列印的結果應該是jack–>t4–>-t3—>t2---->t1—>t0;
在這里插入圖片描述

但是這其實不能說明問題,因為誠如我上文說的notifyAll如果是即時喚醒也是這個列印順序;但是如果改下代碼就能看出問題了,把喚醒jack放到啟動五個執行緒前面;

package com.shadow.test;


import lombok.extern.slf4j.Slf4j;
import org.omg.CORBA.TIMEOUT;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "s")
public class TestWait2 {

    static  Object object = new Object();//鎖物件
    static boolean isWoman = false; // 是否有女人

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {//jack
            synchronized (object){
                while (!isWoman){//判斷是否有女人
                    log.debug("沒有女人 我去等待老板安排 先休息,安排好之后叫醒我");
                    try {

                        //jack執行緒進入阻塞,但是釋放了鎖
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("開始作業啪啪啪啪");
            }
        }, "jack").start();


        TimeUnit.SECONDS.sleep(1);

        log.debug("1s之后主執行緒獲取鎖");
        log.debug("因為jack wait釋放了鎖,主執行緒能夠獲取到");
        log.debug("-----------關鍵看列印順序----------------");
        synchronized (object) {

           ------------------------------------- //注意順序換了------------------------
            isWoman=true;
            object.notifyAll();


            for (int i = 0; i < 5; i++) {
                new Thread(() -> {
                    synchronized (object) {
                        log.debug("那些默默無聞的程式員coding");
                    }
                }, "t" + i).start();
                TimeUnit.MILLISECONDS.sleep(1);
            }


        }


    }

}

那么情況就不同了如下圖

在這里插入圖片描述

那么執行順序也就變了,jack的執行應該在最后(synchronized是倒序的);

在這里插入圖片描述

這樣應該足以證明問題了吧;假設你還不信,那么可以來翻閱JDK原始碼來證明這個問題,當然咯翻閱JDK原始碼主要是看ReentrantLock的實作,而不是wait的實作,wait的原始碼需要去看JVM原始碼C++代碼;我們這里翻閱JDK原始碼只是來看看ReentrantLock當中的Condition是如何實作的,他幾乎和wait的原理一樣

把上面那個jack和rose加班的例子改一下,改成用ReentrantLock來實作(注意看代碼)

package com.shadow.test;


import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j(topic = "s")
public class TestCondition1 {

    static ReentrantLock lock = new ReentrantLock();//鎖物件
    static boolean isWoman = false; // 是否有女人
    static boolean isMoney = false; // 是否加錢了
    //因為錢不滿足而進入阻塞的佇列
    static Condition conditionMoney = lock.newCondition();
    //因為女人不滿足而進入阻塞的佇列
    static Condition conditionWoman = lock.newCondition();


    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {//jack
            lock.lock();
                while (!isWoman){//判斷是否有女人
                    log.debug("沒有女人 我去等待老板安排 先休息,安排好之后叫醒我");
                    try {

                        //jack執行緒進入阻塞,但是釋放了鎖
                        //進入特定的佇列---conditionWoman
                       conditionWoman.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("開始作業啪啪啪啪");
           lock.unlock();
        }, "jack").start();

        new Thread(() -> {
           lock.lock();
                while (!isMoney){
                    log.debug("沒有加錢,先休息不干活");
                    try {
                        // 進入特定的佇列---conditionWoman
                        conditionMoney.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("干活了----有錢能使鬼推");
            lock.unlock();
        }, "rose").start();



        TimeUnit.SECONDS.sleep(5);
        new Thread(() -> {//jack
            lock.lock();
               isWoman=true;
               log.debug("jack 橋本有菜來了,你可以啪啪啪了");
               log.debug("因為是女人滿足,從特定的佇列當中(conditionWoman)喚醒");
               conditionWoman.signalAll();
            lock.unlock();
        }, "boss").start();

    }
}

這里定義了兩個條件佇列相當于定義了兩個休息室,不同條件進入不同休息室,jack進入conditionWoman,而rose進入conditionMoney,老板最后只滿足了橋本有菜故而只喚醒jack——conditionWoman.signalAll();rose是感覺不到的——甚至都不會醒來,剛剛synchronized關鍵字使用wait,呼叫notifyAll會把rose也叫醒,雖然rose最后還是去阻塞了(因為條件還是沒有滿足),但是終究是被叫醒了一次;使用ReentrantLock就沒有這個問題了

在這里插入圖片描述
自此可以知道ReentrantLock當中Condition可以實作和synchronized的wait一樣的功能,而且她支持多條件,這比wait更加豐富(也是一個經典面試題——ReentrantLock和synchronze關鍵字的區別,什么情況下使用),你可以從這個方面去回答的,當然他們的區別不止這些,有機會在討論吧

關鍵他的原始碼是怎樣呢?也就是呼叫conditionWoman.await();是否如我所說會進入到一個waitSet的佇列?
在這里插入圖片描述

如果你有印象前面我們分析過所有因為拿不到鎖的物件會進入到一個佇列當中(ReentantLock當中的 FairSync這個物件所維護的雙向鏈表佇列),但是這里呼叫conditionWoman.await();則執行緒進入的是ConditionObject這個物件所維護的雙向鏈表佇列;這里足可證明waiting和blokcing兩種不同狀態的執行緒是維護在不同佇列的(需要注意的是waitSet和EntryList是C++中的說法,由于ReentrantLock是java語言實作的,故而EntryList對應是ReentrantLock當中公平鎖或者非公平鎖中的佇列,WaitSet對應的是ConditionObject這個雙向鏈表佇列);

再來看一下ReentrantLock的喚醒方法conditionWoman.signalAll();

在這里插入圖片描述
結論:呼叫signalAll或者notifyAll方法都是把waiting狀態的執行緒轉化成為blocking狀態——因為JDK內部是完成了一個佇列的轉移,而不是即時喚醒執行

當然最后的最后我們再次來看看ReentrantLock的Condition被喚醒之后的呼叫順序是怎樣的

package com.shadow.test;


import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j(topic = "s")
public class TestConddition2 {

    static ReentrantLock lock = new ReentrantLock();//鎖物件
    static boolean isWoman = false; // 是否有女人
    static Condition conditoon = lock.newCondition();
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {//jack
            lock.lock();
                while (!isWoman){//判斷是否有女人
                    log.debug("沒有女人 我去等待老板安排 先休息,安排好之后叫醒我");
                    try {

                        //jack執行緒進入阻塞,但是釋放了鎖
                        conditoon.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("開始作業啪啪啪啪");
            lock.unlock();
        }, "jack").start();


        TimeUnit.SECONDS.sleep(1);

        log.debug("1s之后主執行緒獲取鎖");
        log.debug("因為jack wait釋放了鎖,主執行緒能夠獲取到");
        log.debug("-----------關鍵看列印順序----------------");
        lock.lock();



            for (int i = 0; i < 5; i++) {
                new Thread(() -> {
                   lock.lock();
                        log.debug("那些默默無聞的程式員coding");
                   lock.unlock();
                }, "t" + i).start();
                TimeUnit.MILLISECONDS.sleep(1);
            }

        //注意順序換了------------------------
        isWoman=true;
        conditoon.signalAll();



        lock.unlock();


    }
}

首先jack獲取到了鎖,然后呼叫了 conditoon.await();方法進入了waitSet(ConditionObject維護的雙向鏈表)阻塞,然后主執行緒獲取了鎖,主執行緒獲取鎖之后啟動了5個執行緒(t0-t4);這5個執行緒也要獲取鎖,但是因為現在鎖被主執行緒持有所以這五個執行緒肯定是獲取不到鎖的,繼而進入EntryList(非公平鎖或者公平鎖維護的雙向鏈表佇列)阻塞;啟動完5個執行緒之后主執行緒滿足條件isWoman=true;繼而喚醒jack因為jack是在五個執行緒之后被喚醒的,也就是最后進入佇列的 t0、t1、t2、t3、t4、jack,所以按照ReentrantLock先進先出的規則jack最后執行(和synchronized相反,前面我們已經證明過了synchronized是jack先執行)

在這里插入圖片描述

一篇文章從晚上10點寫到了早上八點寫一個通宵,碼字真的很難,所以更新的慢,哈哈,不知道能不能解答讀者的一二困惱;還有我真心覺得并發編程要是你能把這兩個佇列搞懂學起來就不難了,而且會滋生很大的興趣,筆者以前剛接觸執行緒的時候非常的困擾,覺得執行緒好難,并發好難,鎖好晦澀;但是記得一個不太明媚的早上我自己通過各種閱讀和除錯搞清楚了waitset和EntryList之后便覺得JDK的這些設計好精妙,從此對閱讀原始碼有近乎瘋狂的追求,所以希望你讀完這篇文章之后能有所識訓吧,過兩天我會在b站邊界線java上面更新一個視頻檔案主要就是講這篇文章的知識點,最后提供文章中所有的代碼讀者可以自行除錯;如果大家對并發編程感興趣記得留言我盡快更新第二篇哈哈

代碼地址(代碼中用了lombok,自己裝在idea)
鏈接:https://pan.baidu.com/s/1djfBWmqotbUWimZ1ixFvMQ
提取碼:3v3u
復制這段內容后打開百度網盤手機App,操作更方便哦

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/237698.html

標籤:其他

上一篇:馬原復習綱要

下一篇:手把手教你1分鐘接入騰訊地圖

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more