主頁 > 軟體設計 > ConcurrentHashMap(細節篇)

ConcurrentHashMap(細節篇)

2020-12-12 10:25:13 軟體設計

本篇文章接上篇文章,介紹一些ConcurrentHashMap的細節,雖然不是特別重要,但也是干貨滿滿,
先列一個引數:

 //0:表示counterCells無人使用
 //因為counterCells是一個陣列,執行緒不安全的
 //所以在操作counterCells之前,都要用CAS將cellsBusy改成1,表示有人在使用該陣列
 private transient volatile int cellsBusy;

方法串列

  • spread():
  • addCount的第一部分:
    • 判斷是否有失敗的風險
    • 判斷是否可以投機
      • fullAddCount():
        • counterCells != null,找到的下標 == n
        • 找到的下標 != null
        • counterCells == null
          • ThreadLocalRandom.getProbe():
          • ThreadLocalRandom.localInit():
          • ThreadLocalRandom.advanceProbe():
  • 面試題:concurrentHashMap的key和value為啥不能為null

spread():

spread(int h):

		//跟hashmap有點區別,不僅高16參與了計算,并且還再次 & 計算
        return (h ^ (h >>> 16)) & HASH_BITS;

前面運算這里不解釋,跟hashMap一樣,主要看后面的& HASH_BITS

        HASH_BITS是0x7fffffff,換算下來是2147483649int的最大值
        作用是消除負數,這里順便看下負數的二進制
        舉個例子:
                -5的原碼是:10000000000000000000000000000101
                    反碼是:11111111111111111111111111111010
                    補碼是:11111111111111111111111111111011

負數的原碼:是對應的正數的二進制,但是符號位是1,
符號位是區分正負數用的,1是負數,0是整數(符號位就是最高位的意思,左邊第一位)
負數的反碼:是除符號位,其余的取反
負數的補碼:是反碼+1
HASH_BITS換算下來的二進制是:11111111111111111111111111111111(注意是31位,雖然Java中的int是32位,但是一般只有負數才會顯示32位,正數默認31位)
hash & HASH_BITS,不管hash是正還是負,結果都會轉成正數,因為HASH_BITS的符號位是0,& 下來的最高位肯定是0,也就是正數,
spread(int h) 的作用其實就是避免hash值是負數,大概是因為ConcurrentHashMap內置了MOVEDTREEBINRESERVED這3個hash,是負數,為了避免沖突吧,

addCount的第一部分:

        //判斷是否有失敗的風險
        if ((as = counterCells) != null ||
                !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            //判斷是否可以投機
            if (as == null || (m = as.length - 1) < 0 ||
                    (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                    !(uncontended =
                            U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                  //死回圈把x記錄到counterCells,uncontended是判斷有沒有嘗試用CAS更新過
                fullAddCount(x, uncontended);
                return;
            }
              //到這表示CAS把值記到counterCells肯定成功了,因為上面的是 ||,CAS是最后一個判斷條件
              //check == 0:表示插入的下標是空的,此時是頭節點,此坑位還有不少直接return
              //check == 1:表示插入的是鏈表的第2個,此坑位還有不少,就不考慮擴容,直接return
              //check == -1:表示的是洗掉,直接return
              //這里跟hashMap有點不一樣,沒有直接判斷大于多少就擴容
            if (check <= 1)
                return;
              //把counterCells的值累加到baseCount
            s = sumCount();
        }

先介紹一下counterCells,每次是CAS去增加baseCount的,這樣并發的話肯定會有執行緒更新失敗,這時候ConcurrentHashMap并沒有重試,而是把失敗的值記錄下來,就是記錄到counterCells里面,
這里可以分為2步:

  1. 判斷是否有失敗的風險
  2. 判斷是否可以投機(開玩笑)

判斷是否有失敗的風險

if ((as = counterCells) != null ||
      !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {

(as = counterCells) != null:

很簡單,counterCells初始為null,CAS增加baseCount失敗就會把值記錄到counterCells里
counterCells != null說明出現并發修改baseCount,有執行緒失敗了
已經有執行緒失敗了,那么再CAS新增很有可能失敗,所有這里 != null 就沒有嘗試CAS,直接走下面的邏輯

為null才走CAS,盡可能的避免競爭

判斷是否可以投機

//進到這里,表示很有可能多執行緒在修改baseCount
//ThreadLocalRandom.getProbe()可以簡單的先理解成取當前執行緒的hash,后面再詳細分析
//非常Nice的判斷
if (as == null || (m = as.length - 1) < 0 ||
      (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
      !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
    //死回圈把x記錄到counterCells,uncontended是判斷有沒有嘗試用CAS更新過
  fullAddCount(x, uncontended);
  return;
}

as == null || (m = as.length - 1) < 0:

這個判斷很簡單,判斷counterCells是否有值,但是含義卻不簡單
因為上面已經判斷過 != null了,這里再判斷一下,為了減少競爭,繼續看下面的判斷

(a = as[ThreadLocalRandom.getProbe() & m]) == null:

ThreadLocalRandom.getProbe()最后說,先簡單理解成執行緒的hash
這里判斷的是根據當前執行緒推算出要插入的counterCells的下標是否為null
走到這里,counterCells!= null,再判斷插入的下標是否= null

U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x):

因為if里面是 || 判斷,走到這,說明counterCells != null,要插入的counterCells的下標 =null
實在沒辦法了,必須得競爭了,此時才CAS,把值更新到counterCells 里面

回憶一下,整個判斷,是這樣的:

  1. counterCells == null,CAS更新baseCount
  2. counterCells != null(或者CAS失敗),再判斷一遍counterCells != null
  3. counterCells != null,再判斷要插入的下標!= null
  4. 要插入的下標!= null,才CAS把值更新到counterCells

可以看到,盡量的保證進入到fullAddCount這個死回圈的時候,沒有競爭,
實在避免不了,再進死回圈之前,再嘗試一遍CAS,非常nice,Doug Lea牛逼,

fullAddCount():

繼續fullAddCount
由于較為復雜,分成3部分看:

  1. counterCells != null,找到的下標 == null
  2. 找到的下標 != null
  3. counterCells == null

counterCells != null,找到的下標 == n

private final void fullAddCount(long x, boolean wasUncontended) {
        int h;
          //判斷當前執行緒的PROBE是否初始化過
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
              //為0就先初始化
            ThreadLocalRandom.localInit();
              //獲取值
            h = ThreadLocalRandom.getProbe();
              //把標志位置為true,確保為true,也就是確保就是沒有CAS過
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            CounterCell[] as; CounterCell a; int n; long v;
              //先判斷counterCells是否初始化過
            if ((as = counterCells) != null && (n = as.length) > 0) {
                  //h是執行緒的隨機值,(n - 1) & h就是計算下標
                if ((a = as[(n - 1) & h]) == null) {
                      //cellsBusy為0,表示沒執行緒在初始化或者擴容counterCells
                    if (cellsBusy == 0) {           
                          //生成要插入的物件
                        CounterCell r = new CounterCell(x); 
                          //把cellsBusy改成1,表示有執行緒再用
                        if (cellsBusy == 0 &&
                                U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                            boolean created = false;
                            try { 
                                CounterCell[] rs; int m, j;
                                  //把cellsBusy改成1,再判斷一下
                                  //因為改成1之后,counterCells就鎖定了,此時的判斷就是準的
                                if ((rs = counterCells) != null &&
                                        (m = rs.length) > 0 &&
                                        rs[j = (m - 1) & h] == null) {
                                      //賦值
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                  //最終將cellsBusy置為0
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;
                        }
                    }
                    collide = false;
                }

總的來說,還是比較簡單的,就是一個普通陣列,用cellsBusy來保證執行緒安全,
不過有一個判斷比較巧妙,需要單獨說一下:

if ((h = ThreadLocalRandom.getProbe()) == 0) {
		//記住這個變數
		wasUncontended = true;

這里不僅僅是判斷PROBE是否初始化過,還有一個意義wasUncontended

其實這里判斷的是進方法之前,有沒有CAS過
== 0說明沒有初始化,說明進fullAddCount是因為counterCells為null
!= 0說明初始化過了,說明進fullAddCount之前已經走過getProbe()了,也就是很有可能CAS過

wasUncontended == true:說明沒有CAS
wasUncontended == false:說明CAS失敗了

找到的下標 != null

                //走到這,說明計算的下標不為null
                else if (!wasUncontended)
                    //進到里面,說明之前在addCount已經CAS過了,并且失敗了
                    //第一次只把標志位改為true,改個狀態出去,給個機會,第二次再來CAS,避免競爭
                    wasUncontended = true;
                //走到這,2種可能
                //1:是死回圈的第二次了
                //2:是之前沒有CAS過
                //不管哪種情況,必須要CAS了,要么是第一次,要么已經讓過一次了,只能競爭了,
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                	//成功就退出了
                    break;
                //判斷一下counterCells是否已經被改掉了或者說陣列長度已經 >= CPU數量了
                //counterCells 最大是NCPU
                else if (counterCells != as || n >= NCPU)
                    collide = false;
                else if (!collide)
                	//進到里面,說明需要擴容了
                	//改個狀態出去,給個機會
                    collide = true;
                //走到這,沒辦法了,準備擴容吧
                //嘗試CAS標志cellsBusy
                else if (cellsBusy == 0 &&
                        U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    try {
                        //雙重校驗,一個意思
                        if (counterCells == as) {
                            //擴容2倍
                            CounterCell[] rs = new CounterCell[n << 1];
                            //把舊值復制到新陣列
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            counterCells = rs;
                        }
                    } finally {
                        //最終將cellsBusy置為0
                        cellsBusy = 0;
                    }
                    //改為false,下次還要擴容又要2次判斷
                    collide = false;
                    //跳出,繼續嘗試把值新增到陣列
                    continue;
                }
                //每次不成功就算下新的hash,換個下標試試
                h = ThreadLocalRandom.advanceProbe(h);
            }
           
        }
    }

同樣不是很難,只是有一點確實值得借鑒,concurrentHashMap真的是極力避免競爭,上面說的給個機會,其實相當于空輪詢,競爭失敗之后,基本上都要空輪詢一次,第2次才會繼續競爭,

counterCells == null

			//走到這,說明counterCells為null
            //就嘗試把CELLSBUSY改完1
            else if (cellsBusy == 0 && counterCells == as &&
                    U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                boolean init = false;
                try {                           // Initialize table
                    //雙重校驗,一個意思
                    if (counterCells == as) {
                        //初始化長度為2
                        CounterCell[] rs = new CounterCell[2];
                        //h是執行緒的隨機值,h & 1就是計算下標
                        rs[h & 1] = new CounterCell(x);
                        counterCells = rs;
                        //設定初始化狀態init為true
                        init = true;
                    }
                } finally {
                    //最終將cellsBusy置為0
                    cellsBusy = 0;
                }
                if (init)
                    //初始化好了,且新增好了就跳出
                    break;
            }
            //走到這,說明已經有執行緒在初始化counterCells,就再嘗試一下baseCount + 1,不行就死回圈
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;

這也不難,就是普通 陣列,執行緒安全的初始化,
拆開來,貌似都不難,但是值得借鑒的確實多,

ThreadLocalRandom.getProbe():
UNSAFE.getInt(Thread.currentThread(), PROBE)

很簡單,就是獲取當前執行緒的某個變數值
看下PROBE

    private static final sun.misc.Unsafe UNSAFE;
    private static final long PROBE;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> tk = Thread.class;
                (tk.getDeclaredField("threadLocalRandomProbe"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }

是獲取的threadLocalRandomProbe的初始地址的偏移量,簡單理解成獲取值,
看下threadLocalRandomProbe:

    @sun.misc.Contended("tlr")
    int threadLocalRandomProbe;

就是一個變數,那可以肯定,第一次ThreadLocalRandom.getProbe()肯定為0

ThreadLocalRandom.localInit():
	private static final int PROBE_INCREMENT = 0x9e3779b9;
	private static final AtomicInteger probeGenerator = new AtomicInteger();
    static final void localInit() {
    	//很簡單,獲取一個值0x9e3779b9,-1640531527
        int p = probeGenerator.addAndGet(PROBE_INCREMENT);
        int probe = (p == 0) ? 1 : p;
        //賦值
        UNSAFE.putInt(t, PROBE, probe);
    }

說明一下:
threadLocalRandomProbe是Thread的變數
probeGenerator 、PROBE_INCREMENTThreadLocalRandom的,每調一次localInitprobeGenerator的值也在變,
看完整個PROBE的程序,理解成執行緒的hash是沒啥問題的,

ThreadLocalRandom.advanceProbe():
	static final int advanceProbe(int probe) {
        probe ^= probe << 13;   // xorshift
        probe ^= probe >>> 17;
        probe ^= probe << 5;
        UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
        return probe;
    }

一通計算,PROBE肯定變了,每次換個值,也就是換個下標,

面試題:concurrentHashMap的key和value為啥不能為null

先來回憶一下hashMap:

	static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

concurrentHashMap:

	final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();

可以看到,hashMap是允許的,concurrentHashMap不允許,

先說value不允許為null:
concurrentHashMap定位的是并發場景,如果允許valuenull,那么就會造成一種情況,

A執行緒:get(key)null,這是A是不知道keyvaluenull,還是key不存在
如果是hashMap,可以用contains來判斷,而concurrentHashMap不行,
B執行緒:在Aget(key) 之后put(key,null)
此時Acontains就是true,那么就以為key當時是有值的,不知道是B中途插入的,
hashMap定位的是單執行緒,containstrue,就表示為key當時是有值的,不考慮B中途插入的情況,

有人會說,別的value不為null,也會發生這種情況,
沒錯,但是get(key)nullcontainstrue,A能知道有人中途插入了,
先說key不允許為null:
value不允許為null:同樣的原因,containsKey的時候有二義性

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

標籤:其他

上一篇:redux react-redux簡介

下一篇:一文讓你徹底了解Redis(進階),史上最全,不看后悔!!!【建議收藏】

標籤雲
其他(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