欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java面試??贾瓹oncurrentHashMap多線程擴(kuò)容機(jī)制詳解

 更新時間:2023年05月23日 10:29:16   作者:愛敲代碼的小黃  
幾乎所有的后端技術(shù)面試官都要在?ConcurrentHashMap?技術(shù)的使用和原理方面對小伙伴們進(jìn)行刁難,本文主要來和大家聊聊ConcurrentHashMap多線程的擴(kuò)容機(jī)制,希望對大家有所幫助

一、引言

ConcurrentHashMap 技術(shù)在互聯(lián)網(wǎng)技術(shù)使用如此廣泛,幾乎所有的后端技術(shù)面試官都要在 ConcurrentHashMap 技術(shù)的使用和原理方面對小伙伴們進(jìn)行 360° 的刁難。

作為一個在互聯(lián)網(wǎng)公司面一次拿一次 Offer 的面霸,打敗了無數(shù)競爭對手,每次都只能看到無數(shù)落寞的身影失望的離開,略感愧疚(請允許我使用一下夸張的修辭手法)。

本篇文章大約讀取時間十分鐘,帶你徹底掌握 ConcurrentHashMap魔鬼式的多線程擴(kuò)容 邏輯,目錄如下:

二、擴(kuò)容流程

我們上一篇文章講了關(guān)于 ConcurrentHashMap 的存儲流程:聊聊ConcurrentHashMap的存儲流程,熟悉了其數(shù)據(jù)是如何的放到數(shù)組、鏈表、紅黑樹中

今天這篇文章,我們重點講一下 ConcurrentHashMap 魔鬼式的多線程擴(kuò)容邏輯

系好安全帶,我們發(fā)車了!

1、treeifyBin方法觸發(fā)擴(kuò)容

// 在鏈表長度大于等于8時,嘗試將鏈表轉(zhuǎn)為紅黑樹
private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    // 數(shù)組不能為空
    if (tab != null) {
        // 數(shù)組的長度n,是否小于64
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            // 如果數(shù)組長度小于64,不能將鏈表轉(zhuǎn)為紅黑樹,先嘗試擴(kuò)容操作
            // 這里的擴(kuò)容傳入的數(shù)據(jù)是當(dāng)前數(shù)據(jù)的2倍
            tryPresize(n << 1);
        // 省略部分代碼……
    }
}

2、tryPreSize方法-針對putAll的初始化操作

private final void tryPresize(int size) {
    // 如果當(dāng)前傳過來的size大于最大值MAXIMUM_CAPACITY >>> 1,則取 MAXIMUM_CAPACITY 
    // 反之取 tableSizeFor(size + (size >>> 1) + 1)= size的1.5倍 + 1
    // tableSizeFor:這個方法我們之前也講過,找size的最近二次冪
    // 這個地方的c
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(size + (size >>> 1) + 1);
    int sc;
    // 當(dāng)前的sizeCtl大于等于0,證明不屬于異常狀態(tài)
    // static final int MOVED     = -1; // 代表當(dāng)前hash位置的數(shù)據(jù)正在擴(kuò)容!
    // static final int TREEBIN   = -2; // 代表當(dāng)前hash位置下掛載的是一個紅黑樹
    // static final int RESERVED  = -3; // 預(yù)留當(dāng)前索引位置……
    while ((sc = sizeCtl) >= 0) {
        Node<K,V>[] tab = table; int n;
        // 如果當(dāng)前的數(shù)組為空,走初始化的邏輯
        // 有人可能會好奇,這里怎么會為空呢?
        // 大家可以去看下putAll的源碼(放在下面),源碼里面第一句調(diào)用的就是tryPresize的方法
        if (tab == null || (n = tab.length) == 0) {
            // 進(jìn)來執(zhí)行初始化
            // sc是初始化長度,初始化長度如果比計算出來的c要大的話,直接使用sc,如果沒有sc大,
            // 說明sc無法容納下putAll中傳入的map,使用更大的數(shù)組長度
            n = (sc > c) ? sc : c;
            // 還是老配方,CAS將SIZECTL嘗試從sc變成-1
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    // Step1:判斷引用
                    // Step2:創(chuàng)建數(shù)組
                    // Step3:修改sizeCtl數(shù)值
                    if (table == tab) {
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
            }
        }
        // 如果計算出來的長度c如果小于等于sc,直接退出循環(huán)結(jié)束方法
        // 數(shù)組長度大于等于最大長度了,直接退出循環(huán)結(jié)束方法
        // 不需要擴(kuò)容
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
        // 省略部分代碼
    }
}
// putAll方法
public void putAll(Map<? extends K, ? extends V> m) {
    // 直接走擴(kuò)容的邏輯
    tryPresize(m.size());
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        putVal(e.getKey(), e.getValue(), false);
}

3、tryPreSize方法-計算擴(kuò)容戳

private final void tryPresize(int size) {
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
        tableSizeFor(size + (size >>> 1) + 1);
    int sc;
    while ((sc = sizeCtl) >= 0) {
        Node<K,V>[] tab = table;
        // 當(dāng)前數(shù)組的長度
        int n; 
        // 判斷引用是否一致
        if (tab == table) {
            // 計算擴(kuò)容標(biāo)識戳,根據(jù)當(dāng)前數(shù)組的長度計算一個16位的擴(kuò)容戳
            // 1、保證后面的SIZECTL賦值是負(fù)數(shù)
            // 2、記錄當(dāng)前從什么長度開始擴(kuò)容
            int rs = resizeStamp(n);
            // 這里存在bug
            // 由于我們上面(sc = sizeCtl) >= 0的判斷,sc只能大于0
            // 這里永遠(yuǎn)大于等于0
            if (sc < 0) {
                // 協(xié)助擴(kuò)容的代碼
            }
            // 代表當(dāng)前沒有線程進(jìn)行擴(kuò)容,我是第一個擴(kuò)容的
            else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
        }
    }
}
// 計算擴(kuò)容標(biāo)識戳
static final int resizeStamp(int n) {
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}

4、tryPreSize方法-對sizeCtl的修改

private final void tryPresize(int size) {
    // sc默認(rèn)為sizeCtl
    while ((sc = sizeCtl) >= 0) {
        else if (tab == table) {
            // rs:擴(kuò)容戳  00000000 00000000 10000000 00011010
            int rs = resizeStamp(n);
            if (sc < 0) {
                // 說明有線程正在擴(kuò)容,過來幫助擴(kuò)容
                // 我們之前第一個線程擴(kuò)容的時候,將sizeCtl設(shè)置成:10000000 00011010 00000000 00000010
                // 所以:sc = 10000000 00011010 00000000 00000010
                Node<K,V>[] nt;
                // 依然有BUG
                // 當(dāng)前線程擴(kuò)容時,老數(shù)組長度是否和我當(dāng)前線程擴(kuò)容時的老數(shù)組長度一致
                // 00000000 00000000 10000000 00011010
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs  
                    // 10000000 00011010 00000000 00000010 
                    // 00000000 00000000 10000000 00011010
                    // 這兩個判斷都是有問題的,核心問題就應(yīng)該先將rs左移16位,再追加當(dāng)前值。
                    // 這兩個判斷是BUG
                    // 判斷當(dāng)前擴(kuò)容是否已經(jīng)即將結(jié)束
                    || sc == rs + 1   // sc == rs << 16 + 1 BUG
                    // 判斷當(dāng)前擴(kuò)容的線程是否達(dá)到了最大限度
                    || sc == rs + MAX_RESIZERS   // sc == rs << 16 + MAX_RESIZERS BUG
                    // 擴(kuò)容已經(jīng)結(jié)束了。
                    || (nt = nextTable) == null 
                    // 記錄遷移的索引位置,從高位往低位遷移,也代表擴(kuò)容即將結(jié)束。
                    || transferIndex <= 0)
                    break;
                // 如果線程需要協(xié)助擴(kuò)容,首先就是對sizeCtl進(jìn)行+1操作,代表當(dāng)前要進(jìn)來一個線程協(xié)助擴(kuò)容
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    // 上面的判斷沒進(jìn)去的話,nt就代表新數(shù)組
                    transfer(tab, nt);
            }
            // 是第一個來擴(kuò)容的線程
            // 基于CAS將sizeCtl修改為:10000000 00011010 00000000 00000010 
            // 將擴(kuò)容戳左移16位之后,符號位是1,就代碼這個值為負(fù)數(shù)
            // 低16位在表示當(dāng)前正在擴(kuò)容的線程有多少個:低16位為2,代表當(dāng)前有一個線程正在擴(kuò)容
            // 為什么低16位值為2時,代表有一個線程正在擴(kuò)容
            // 每一個線程擴(kuò)容完畢后,會對低16位進(jìn)行-1操作,當(dāng)最后一個線程擴(kuò)容完畢后,減1的結(jié)果還是-1,
            // 當(dāng)值為-1時,要對老數(shù)組進(jìn)行一波掃描,查看是否有遺漏的數(shù)據(jù)沒有遷移到新數(shù)組
            else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2))
                // 調(diào)用transfer方法,并且將第二個參數(shù)設(shè)置為null,就代表是第一次來擴(kuò)容!
                transfer(tab, null);
        }
    }
}

5、transfer方法-計算每個線程遷移的長度

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // 數(shù)組長度	
    int n = tab.length; 
    // 每個線程一次性遷移多少數(shù)據(jù)到新數(shù)組
    int stride; 		
    // 基于CPU的線程數(shù)來計算:NCPU = Runtime.getRuntime().availableProcessors()
    // MIN_TRANSFER_STRIDE = 16
    // NCPU = 4
    // 舉個例子:N = 1024 - 512 - 256 - 128 / 4 = 32
    // 如果算出來每個線程的長度小于16的話,直接使用16
    // 如果大于16的話,則使用N
    // 如果線程數(shù)只有1的話,直接就是原數(shù)組長度
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE){
        stride = MIN_TRANSFER_STRIDE;
    }
}

6、 transfer方法-構(gòu)建新數(shù)組并查看標(biāo)識屬性

// 以32位長度數(shù)組擴(kuò)容到64位為例子
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // 數(shù)組長度	
    int n = tab.length; 
    // 每個線程一次性遷移多少數(shù)據(jù)到新數(shù)組
    int stride; 	
    // 第一個進(jìn)來擴(kuò)容的線程需要把新數(shù)組構(gòu)建出來
    if (nextTab == null) {            
        try {
            // 將原數(shù)組長度左移一位創(chuàng)建新數(shù)組
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            // 賦值操作
            nextTab = nt;
        } catch (Throwable ex) {      
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        // 將成員變量的新數(shù)組賦值volatile修飾
        nextTable = nextTab;
        // 遷移數(shù)據(jù)時用到的標(biāo)識volatile修飾
        transferIndex = n;
    }
    // 新數(shù)組長度
    int nextn = nextTab.length;
    // 老數(shù)組遷移完數(shù)據(jù)后,做的標(biāo)識
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    // 遷移數(shù)據(jù)時,需要用到的標(biāo)識
    boolean advance = true;
    boolean finishing = false; 
}

7、transfer方法-線程領(lǐng)取遷移任務(wù)

// 以32位長度數(shù)組擴(kuò)容到64位為例子
// 數(shù)組長度	
int n = tab.length; 
// 每個線程一次性遷移多少數(shù)據(jù)到新數(shù)組
int stride;
// 遷移數(shù)據(jù)時用到的標(biāo)識volatile修飾
transferIndex = n;
// 新數(shù)組長度
int nextn = nextTab.length;
// 老數(shù)組遷移完數(shù)據(jù)后,做的標(biāo)識
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// advance:true,代表當(dāng)前線程需要接收任務(wù),然后再執(zhí)行遷移
// 如果為false,代表已經(jīng)接收完任務(wù)
boolean advance = true;
// 標(biāo)識遷移是否結(jié)束
boolean finishing = false;
// 開始循環(huán)
for (int i = 0, bound = 0;;) {
    Node<K,V> f; 
    int fh;
    while (advance) {
        int nextIndex;
        int nextBound;
        // 第一次:這里肯定進(jìn)不去
        // 主要判斷當(dāng)前任務(wù)是否執(zhí)行完畢
        if (--i >= bound || finishing)
            advance = false;
        // 第一次:這里肯定也進(jìn)不去
        // 判斷transferIndex是否小于等于0,代表沒有任務(wù)可領(lǐng)取,結(jié)束了。
        // 在線程領(lǐng)取任務(wù)會,會對transferIndex進(jìn)行修改,修改為transferIndex - stride
        // 在任務(wù)都領(lǐng)取完之后,transferIndex肯定是小于等于0的,代表沒有遷移數(shù)據(jù)的任務(wù)可以領(lǐng)取
        else if ((nextIndex = transferIndex) <= 0) {
            i = -1;
            advance = false;
        }
       	// nextIndex=32
        // stride=16
        // nextIndex > stride ? nextIndex - stride : 0:當(dāng)前的nextIndex是否大于每個線程切割的
        // 是:nextIndex - stride/否:0
        // 將TRANSFERINDEX從nextIndex變成16
        else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
                                     nextBound = (nextIndex > stride ?nextIndex - stride : 0))) {
            // 對bound賦值(16)
            bound = nextBound;
            // 對i賦值(31)
            i = nextIndex - 1;
            // 賦值,代表當(dāng)前領(lǐng)取任務(wù)結(jié)束
            // 該線程當(dāng)前領(lǐng)取的任務(wù)是(16~31)
            advance = false;
        }
    }
}

線程領(lǐng)取擴(kuò)容任務(wù)的流程:

8、transfer方法-擴(kuò)容是否已經(jīng)結(jié)束

// 判斷擴(kuò)容是否已經(jīng)結(jié)束!
// i < 0:當(dāng)前線程沒有接收到任務(wù)!
// i >= n: 遷移的索引位置,不可能大于數(shù)組的長度,不會成立
// i + n >= nextn:因為i最大值就是數(shù)組索引的最大值,不會成立
if (i < 0 || i >= n || i + n >= nextn) {
    int sc;
    // 第一次到這里:必定是false
    // 如果再次到這里的時候,最后一個擴(kuò)容的線程也掃描完了
    if (finishing) {
        nextTable = null;
        table = nextTab;
        // 重新更改當(dāng)前的sizeCtl的數(shù)值:0.75N
        sizeCtl = (n << 1) - (n >>> 1);
        return;
    }
    // 到這里代表當(dāng)前線程已經(jīng)不需要去擴(kuò)容了
    // 那么它要把當(dāng)前并發(fā)擴(kuò)容的線程數(shù)減一
    // SIZECTL ----> sc - 1
    if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
        // 判斷當(dāng)前這個線程是不是最后一個擴(kuò)容線程
        if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
            // 如果不是的話,直接返回就好了
            return;
        // 如果是的話,當(dāng)前線程還需要將舊數(shù)據(jù)全部掃描一遍,校驗一下是不是都遷移完了
        finishing = advance = true;
        // 將當(dāng)前的i重新變成原數(shù)組的長度
        // 重新進(jìn)行一輪校驗
        i = n; // recheck before commit 
    }
}

9、 transfer方法-遷移數(shù)據(jù)(鏈表)

    // 如果當(dāng)前的i位置上沒有數(shù)據(jù),直接不需要遷移
	else if ((f = tabAt(tab, i)) == null)
        // 將原來i位置上CAS標(biāo)記成fwd
        // fwd的Hash為Move
        advance = casTabAt(tab, i, null, fwd);
	// // 拿到當(dāng)前i位置的hash值,如果為MOVED,證明數(shù)據(jù)已經(jīng)遷移過了。
    else if ((fh = f.hash) == MOVED)
        // 那么直接返回即可,主要是最后一個擴(kuò)容線程掃描的時候使用
        advance = true; // already processed
    else {
        // 鎖住
        synchronized (f) {
            // 再次校驗
            if (tabAt(tab, i) == f) {
                // ln:null  - lowNode
                Node<K,V> ln;
                 // hn:null  - highNode
                Node<K,V> hn;
                // 如果當(dāng)前的fh是正常的
                if (fh >= 0) {
                    // 求當(dāng)前的runBit
                    // 這里只會有兩個結(jié)果:0 或者 n
                    int runBit = fh & n;
                    // 用當(dāng)前l(fā)astRun指向f
                    Node<K,V> lastRun = f;
                    // 進(jìn)行鏈表的遍歷
                    for (Node<K,V> p = f.next; p != null; p = p.next) {
                        // 求每一個鏈表的p.hash & n
                        int b = p.hash & n;
                        // 如果當(dāng)前的b不等于上一個節(jié)點
                        // 需要更新下runBit的數(shù)據(jù)和lastRun的指針
                        if (b != runBit) {
                            runBit = b;
                            lastRun = p;
                        }
                    }
                    // 如果最后runBit=0的話
                    // lastRun賦值給ln
                    if (runBit == 0) {
                        ln = lastRun;
                        hn = null;
                    }
                    // 如果最后runBit=0的話
                    // lastRun賦值給hn
                    else {
                        hn = lastRun;
                        ln = null;
                    }
                    // 從數(shù)組頭節(jié)點開始一直遍歷到lastRun位置
                    for (Node<K,V> p = f; p != lastRun; p = p.next) {
                        // hash/key/value
                        int ph = p.hash; K pk = p.key; V pv = p.val;
                        // 如果當(dāng)前的是0,則掛到ln下面
                        if ((ph & n) == 0)
                            ln = new Node<K,V>(ph, pk, pv, ln);
                        // 如果當(dāng)前的是1,則掛到hn下面
                        else
                            hn = new Node<K,V>(ph, pk, pv, hn);
                    }
                    // 將新數(shù)組i位置設(shè)置成ln
                    setTabAt(nextTab, i, ln);
                    // 將新數(shù)組i+N位置設(shè)置成hn
                    setTabAt(nextTab, i + n, hn);
                    // 將原來數(shù)組的i位置設(shè)置成fwd,代表遷移完畢
                    setTabAt(tab, i, fwd);
                    // 將advance設(shè)置成true,保證進(jìn)行上面的while循環(huán)
                    // 執(zhí)行上面的i--,進(jìn)行下一節(jié)點的遷移計劃
                    advance = true;
                }
            }
        }
    }
}

9.1 LastRun機(jī)制

假如當(dāng)前我需要將數(shù)組從 16 擴(kuò)容至 32,我們看下原來的結(jié)構(gòu):

當(dāng)我們進(jìn)行下面這一步時:

int runBit = fh & n;
for (Node<K,V> p = f.next; p != null; p = p.next) {
    // 求每一個鏈表的p.hash & n
    int b = p.hash & n;
    // 如果當(dāng)前的b不等于上一個節(jié)點
    // 需要更新下runBit的數(shù)據(jù)和lastRun的指針
    if (b != runBit) {
        runBit = b;
        lastRun = p;
    }
}

圖片如下:紫色代表 0,粉色代表 1

在遷移之前,我們要掌握一個知識,由于我們的數(shù)組是 2 倍擴(kuò)容,所以原始數(shù)據(jù)的數(shù)據(jù)一定會落在新數(shù)組的 2N + 2 的位置

最后將我們的數(shù)組從原始數(shù)組直接遷移過來,由于 lastRun 之后位置的數(shù)據(jù)都是一樣的hash,所以直接全量遷移即可,不需要挨個遍歷,這也是實施 lastRun 的原因,減少時間復(fù)雜度

最終遷移效果如上,遷移完畢,減少 i 的下標(biāo),繼續(xù)遷移下一個數(shù)組的位置。

10、協(xié)助擴(kuò)容

// 如果當(dāng)前的hash是正在擴(kuò)容
if ((fh = f.hash) == MOVED){
     // 進(jìn)行協(xié)作式擴(kuò)容
    tab = helpTransfer(tab, f);
}
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; 
    int sc;
    // 第一個判斷:老數(shù)組不為null
    // 第二個判斷:新數(shù)組不為null  (將新數(shù)組賦值給nextTab)
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        // 拿到擴(kuò)容戳
        int rs = resizeStamp(tab.length);
        // 第一個判斷:fwd中的新數(shù)組,和當(dāng)前正在擴(kuò)容的新數(shù)組是否相等。    
        // 相等:可以協(xié)助擴(kuò)容。不相等:要么擴(kuò)容結(jié)束,要么開啟了新的擴(kuò)容
        // 第二個判斷:老數(shù)組是否改變了。     相等:可以協(xié)助擴(kuò)容。不相等:擴(kuò)容結(jié)束了
        // 第三個判斷:如果正在擴(kuò)容,sizeCtl肯定為負(fù)數(shù),并且給sc賦值
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
            // 判斷擴(kuò)容戳是否一致
            // 看下transferIndex是否小于0,如果小于的話,說明擴(kuò)容的任務(wù)已經(jīng)被領(lǐng)完了
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            // 如果還有任務(wù)的話,CAS將當(dāng)前的SIZECTL加一,協(xié)助擴(kuò)容
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                // 協(xié)助擴(kuò)容的代碼,上面剛剛分析過
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}

三、流程圖

以上就是Java面試常考之ConcurrentHashMap多線程擴(kuò)容機(jī)制詳解的詳細(xì)內(nèi)容,更多關(guān)于Java ConcurrentHashMap的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java基礎(chǔ)強(qiáng)化訓(xùn)練輸入錯誤即結(jié)束進(jìn)程

    Java基礎(chǔ)強(qiáng)化訓(xùn)練輸入錯誤即結(jié)束進(jìn)程

    本文主要介紹了Java編程的基礎(chǔ)知識強(qiáng)化應(yīng)用,文中實例涉及到了許多基礎(chǔ)知識,new對象,控制臺輸入,if語句等。很實用,需要的朋友可以參考下
    2017-09-09
  • SpringBoot中swagger的使用

    SpringBoot中swagger的使用

    這篇文章主要介紹了SpringBoot中swagger的使用,文中有非常詳細(xì)的代碼示例,對正在學(xué)習(xí)swagger的小伙伴們有非常好的幫助,需要的朋友可以參考下
    2021-05-05
  • Java StringUtils字符串分割轉(zhuǎn)數(shù)組的實現(xiàn)

    Java StringUtils字符串分割轉(zhuǎn)數(shù)組的實現(xiàn)

    這篇文章主要介紹了Java StringUtils字符串分割轉(zhuǎn)數(shù)組的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-09-09
  • Java中抽象類和接口的區(qū)別_動力節(jié)點Java學(xué)院整理

    Java中抽象類和接口的區(qū)別_動力節(jié)點Java學(xué)院整理

    java抽象類和接口最本質(zhì)的區(qū)別是接口里不能實現(xiàn)方法--接口中的方法全是抽象方法。抽象類中可實現(xiàn)方法--抽象類中的方法可以不是抽象方法,下文給大家簡單介紹下,需要的的朋友參考下
    2017-04-04
  • JAVA注解相關(guān)知識總結(jié)

    JAVA注解相關(guān)知識總結(jié)

    這篇文章主要介紹了JAVA注解相關(guān)知識,文中講解非常詳細(xì),代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下
    2020-06-06
  • Java Mybatis框架多表操作與注解開發(fā)詳解分析

    Java Mybatis框架多表操作與注解開發(fā)詳解分析

    MyBatis 是一款優(yōu)秀的持久層框架,它支持自定義 SQL、存儲過程以及高級映射。MyBatis 免除了幾乎所有的 JDBC 代碼以及設(shè)置參數(shù)和獲取結(jié)果集的工作。MyBatis 可以通過簡單的 XML 或注解來配置和映射原始類型、接口和 Java POJO為數(shù)據(jù)庫中的記錄
    2021-10-10
  • Java?9?中的模塊Module系統(tǒng)

    Java?9?中的模塊Module系統(tǒng)

    Java?9?引入的模塊是在Java包(package)的基礎(chǔ)上又引入的一個新的抽象層,基于package這一點很重要,這里需要強(qiáng)調(diào)一下,接下來通過本文給大家介紹Java?9?中的模塊Module系統(tǒng),感興趣的朋友一起看看吧
    2022-03-03
  • java學(xué)習(xí)筆記之DBUtils工具包詳解

    java學(xué)習(xí)筆記之DBUtils工具包詳解

    下面小編就為大家分享一篇java學(xué)習(xí)筆記之DBUtils工具包詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-01-01
  • 解決java執(zhí)行cmd命令調(diào)用ffmpeg報錯Concat error - No such filter ''[0,0]''問題

    解決java執(zhí)行cmd命令調(diào)用ffmpeg報錯Concat error - No such filter ''[0,0]

    這篇文章主要介紹了java執(zhí)行cmd命令,調(diào)用ffmpeg報錯Concat error - No such filter '[0,0]'解決方法,本文通過截圖實例代碼說明給大家介紹的非常詳細(xì),對大家的工作或?qū)W習(xí)有一定的參考借鑒價值,需要的朋友可以參考下
    2020-03-03
  • Java如何獲取數(shù)組和字符串的長度(length還是length())

    Java如何獲取數(shù)組和字符串的長度(length還是length())

    這篇文章主要介紹了Java如何獲取數(shù)組和字符串的長度(length還是length()),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-12-12

最新評論