解析ConcurrentHashMap: transfer方法源碼分析(難點(diǎn))
- 上一篇文章介紹過put方法以及其相關(guān)的方法,接下來,本篇就介紹一下transfer這個(gè)方法(比較難),最好能動(dòng)手結(jié)合著源碼進(jìn)行分析,并仔細(xì)理解前面幾篇文章的內(nèi)容~
- 注:代碼分析的注釋中的CASE0、CASE1… ,這些并沒有直接關(guān)聯(lián)關(guān)系,只是為了給每個(gè)if邏輯判斷加一個(gè)標(biāo)識,方便在其他邏輯判斷的地方進(jìn)行引用而已。
再復(fù)習(xí)一下并發(fā)Map的結(jié)構(gòu)圖:
1、transfer方法
transfer
方法的作用是:遷移元素,擴(kuò)容時(shí)table容量變?yōu)樵瓉淼膬杀叮巡糠衷剡w移到其它桶nextTable中。該方法遷移數(shù)據(jù)的流程就是在發(fā)生擴(kuò)容時(shí),從散列表原table中,按照桶位下標(biāo),依次將每個(gè)桶中的元素(結(jié)點(diǎn)、鏈表、樹)遷移到新表nextTable中。
另外還有與其相關(guān)的方法helpTransfer
:協(xié)助擴(kuò)容(遷移元素),當(dāng)線程添加元素時(shí)發(fā)現(xiàn)table正在擴(kuò)容,且當(dāng)前元素所在的桶元素已經(jīng)遷移完成了,則協(xié)助遷移其它桶的元素。
下圖為并發(fā)擴(kuò)容流程圖,在分析源碼前熟悉一下流程:
鏈表遷移示意圖:
下面就開始分析代碼把:
/** * 遷移元素,擴(kuò)容時(shí)table容量變?yōu)樵瓉淼膬杀?,并把部分元素遷移到其它桶nextTable中: */ private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { // n 表示擴(kuò)容之前table數(shù)組的長度 // stride 表示分配給線程任務(wù)的步長 int n = tab.length, stride; // 這里為了方便分析,根據(jù)CUP核數(shù),stride 固定為MIN_TRANSFER_STRIDE 16 if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) // 控制線程遷移數(shù)據(jù)的最小步長(桶位的跨度~) stride = MIN_TRANSFER_STRIDE; // subdivide range // ------------------------------------------------------------------------------ // CASE0:nextTab == null // 條件成立:表示當(dāng)前線程為觸發(fā)本次擴(kuò)容的線程,需要做一些擴(kuò)容準(zhǔn)備工作:(在if代碼塊中) // 條件不成立:表示當(dāng)前線程是協(xié)助擴(kuò)容的線程... if (nextTab == null) { // initiating try { // 創(chuàng)建一個(gè)比擴(kuò)容之前容量n大一倍的新table @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; nextTab = nt; } catch (Throwable ex) { // try to cope with OOME sizeCtl = Integer.MAX_VALUE; return; } // 賦值nextTab對象給 nextTable ,方便協(xié)助擴(kuò)容線程 拿到新表 nextTable = nextTab; // 記錄遷移數(shù)據(jù)整體位置的一個(gè)標(biāo)記,初始值是原table數(shù)組的長度。 // 可以理解為:全局范圍內(nèi)散列表的數(shù)據(jù)任務(wù)處理到哪個(gè)桶的位置了 transferIndex = n; } // 表示新數(shù)組的長度 int nextn = nextTab.length; // fwd節(jié)點(diǎn),當(dāng)某個(gè)桶位數(shù)據(jù)遷移處理完畢后,將此桶位設(shè)置為fwd節(jié)點(diǎn),其它寫線程或讀線程看到后,會有不同邏輯。fwd結(jié)點(diǎn)指向新表nextTab的引用 ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); // 推進(jìn)標(biāo)記(后面講數(shù)據(jù)遷移的時(shí)候再分析~) boolean advance = true; // 完成標(biāo)記(后面講數(shù)據(jù)遷移的時(shí)候再分析~) boolean finishing = false; // to ensure sweep before committing nextTab // i 表示分配給當(dāng)前線程的數(shù)據(jù)遷移任務(wù),任務(wù)執(zhí)行到的桶位下標(biāo)(任務(wù)進(jìn)度~表示當(dāng)前線程的任務(wù)已經(jīng)干到哪里了~) // bound 表示分配給當(dāng)前線程任務(wù)的下界限制。(線程的數(shù)據(jù)遷移任務(wù)先從高位開始遷移,遷移到下界位置結(jié)束) int i = 0, bound = 0; // 自旋~ 非常長的一個(gè)for循環(huán)... for (;;) { // f 桶位的頭結(jié)點(diǎn) // fh 頭結(jié)點(diǎn)的hash Node<K,V> f; int fh; /** * while循環(huán)的任務(wù)如下:(循環(huán)的條件為推進(jìn)標(biāo)記advance為true) * 1.給當(dāng)前線程分配任務(wù)區(qū)間 * 2.維護(hù)當(dāng)前線程任務(wù)進(jìn)度(i 表示當(dāng)前處理的桶位) * 3.維護(hù)map對象全局范圍內(nèi)的進(jìn)度 */ // 整個(gè)while循環(huán)就是在算i的值,過程太復(fù)雜,不用太關(guān)心 // i的值會從n-1依次遞減,感興趣的可以打下斷點(diǎn)就知道了 // 其中n是舊桶數(shù)組的大小,也就是說i從15開始一直減到1這樣去遷移元素 while (advance) { // nextIndex~nextBound分配任務(wù)的區(qū)間: // 分配任務(wù)的開始下標(biāo) // 分配任務(wù)的結(jié)束下標(biāo) int nextIndex, nextBound; // ----------------------------------------------------------------------- // CASE1: // 條件一:--i >= bound // 成立:表示當(dāng)前線程的任務(wù)尚未完成,還有相應(yīng)的區(qū)間的桶位要處理,--i 就讓當(dāng)前線程處理下一個(gè)桶位. // 不成立:表示當(dāng)前線程任務(wù)已完成 或者 未分配 if (--i >= bound || finishing) // 推進(jìn)標(biāo)記設(shè)置為flase advance = false; // ----------------------------------------------------------------------- // CASE2: (nextIndex = transferIndex) <= 0 // transferIndex:可以理解為,全局范圍內(nèi)散列表的數(shù)據(jù)任務(wù)處理到哪個(gè)桶的位置了~ // 前置條件:當(dāng)前線程任務(wù)已完成 或 者未分配,即沒有經(jīng)過CASE1 // 條件成立:表示對象全局范圍內(nèi)的桶位都分配完畢了,沒有區(qū)間可分配了,設(shè)置當(dāng)前線程的i變量為-1 跳出循環(huán),然后執(zhí)行退出遷移任務(wù)相關(guān)的程序 // 條件不成立:表示對象全局范圍內(nèi)的桶位尚未分配完畢,還有區(qū)間可分配~ else if ((nextIndex = transferIndex) <= 0) { i = -1; // 推進(jìn)標(biāo)記設(shè)置為flase advance = false; } // ----------------------------------------------------------------------- // CASE3,其實(shí)就是移動(dòng)i的一個(gè)過程:CAS更新成功,則i從當(dāng)前位置-1,再復(fù)習(xí)一下i的含義: // i 表示分配給當(dāng)前線程的數(shù)據(jù)遷移任務(wù),任務(wù)執(zhí)行到的桶位下標(biāo)(任務(wù)進(jìn)度~表示當(dāng)前線程的任務(wù)已經(jīng)干到哪里了~) // CASE3前置條件(未經(jīng)過CASE1-2):1、當(dāng)前線程需要分配任務(wù)區(qū)間 2.全局范圍內(nèi)還有桶位尚未遷移 // 條件成立:說明給當(dāng)前線程分配任務(wù)成功 // 條件失敗:說明分配任務(wù)給當(dāng)前線程失敗,應(yīng)該是和其它線程發(fā)生了競爭~ else if (U.compareAndSwapInt (this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { bound = nextBound; i = nextIndex - 1; advance = false; } } /** 處理線程任務(wù)完成后,線程退出transfer方法的邏輯: **/ // ------------------------------------------------------------------------- // CASE4: // 條件一:i < 0 // 成立:表示當(dāng)前線程未分配到任務(wù),或已完成了任務(wù) // 條件二、三:一般情況下不會成里~ if (i < 0 || i >= n || i + n >= nextn) { // 如果一次遍歷完成了 // 也就是整個(gè)map所有桶中的元素都遷移完成了 // 保存sizeCtl的變量 int sc; // 擴(kuò)容結(jié)束條件判斷 if (finishing) { // 如果全部桶中數(shù)據(jù)遷移完成了,則替換舊桶數(shù)組 // 并設(shè)置下一次擴(kuò)容門檻為新桶數(shù)組容量的0.75倍 nextTable = null; table = nextTab; sizeCtl = (n << 1) - (n >>> 1); return; } // ------------------------------------------------------------------------- // CASE5:當(dāng)前線程擴(kuò)容完成,把并發(fā)擴(kuò)容的線程數(shù)-1,該操作基于CAS,修改成功返回true // 條件成立:說明,更新 sizeCtl低16位 -1 操作成功,當(dāng)前線程可以正常退出了~ // 如果條件不成立:說明CAS更新操作時(shí),與其他線程發(fā)生競爭了~ if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { // 條件成立:說明當(dāng)前線程不是最后一個(gè)退出transfer任務(wù)的線程 // eg: // 1000 0000 0001 1011 0000 0000 0000 0000 if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) // 擴(kuò)容完成兩邊肯定相等 // 正常退出 return; // 完成標(biāo)記finishing置為true,,finishing為true才會走到上面的if擴(kuò)容結(jié)束條件判斷 // 推進(jìn)標(biāo)記advance置為true finishing = advance = true; // i重新賦值為n // 這樣會再重新遍歷一次桶數(shù)組,看看是不是都遷移完成了 // 也就是第二次遍歷都會走到下面的(fh = f.hash) == MOVED這個(gè)條件 i = n; // recheck before commit } } /** 線程處理一個(gè)桶位數(shù)據(jù)的遷移工作,處理完畢后, 設(shè)置advance為true: 表示繼續(xù)推薦,然后就會回到for,繼續(xù)循環(huán) **/ // ------------------------------------------------------------------------- // CASE6: // 【CASE6~CASE8】前置條件:當(dāng)前線程任務(wù)尚未處理完,正在進(jìn)行中! // 條件成立:說明當(dāng)前桶位未存放數(shù)據(jù),只需要將此處設(shè)置為fwd節(jié)點(diǎn)即可。 else if ((f = tabAt(tab, i)) == null) // 如果桶中無數(shù)據(jù),直接放入FWD,標(biāo)記該桶已遷移: // casTabAt通過CAS的方式去向Node數(shù)組指定位置i設(shè)置節(jié)點(diǎn)值,設(shè)置成功返回true,否則返回false // 即:將tab數(shù)組下標(biāo)為i的結(jié)點(diǎn)設(shè)置為FWD結(jié)點(diǎn) advance = casTabAt(tab, i, null, fwd); // ------------------------------------------------------------------------- // CASE7: (fh = f.hash) == MOVED 如果桶中第一個(gè)元素的hash值為MOVED // 條件成立:說明頭節(jié)f它是ForwardingNode節(jié)點(diǎn), // 則,當(dāng)前桶位已經(jīng)遷移過了,當(dāng)前線程不用再處理了,直接再次更新當(dāng)前線程任務(wù)索引,再次處理下一個(gè)桶位 或者 其它操作~ else if ((fh = f.hash) == MOVED) advance = true; // already processed // ------------------------------------------------------------------------- // CASE8: // 前置條件:**當(dāng)前桶位有數(shù)據(jù)**,而且node節(jié)點(diǎn) 不是 fwd節(jié)點(diǎn),說明這些數(shù)據(jù)需要遷移。 else { // 鎖定該桶并遷移元素:(sync 鎖加在當(dāng)前桶位的頭結(jié)點(diǎn)) synchronized (f) { // 再次判斷當(dāng)前桶第一個(gè)元素是否有修改,(可能出現(xiàn)其它線程搶先一步遷移/修改了元素) // 防止在你加鎖頭對象之前,當(dāng)前桶位的頭對象被其它寫線程修改過,導(dǎo)致你目前加鎖對象錯(cuò)誤... if (tabAt(tab, i) == f) { // 把一個(gè)鏈表拆分成兩個(gè)鏈表(規(guī)則按照是桶中各元素的hash與桶大小n進(jìn)行與操作): // 等于0的放到低位鏈表(low)中,不等于0的放到高位鏈表(high)中 // 其中低位鏈表遷移到新桶中的位置相對舊桶不變 // 高位鏈表遷移到新桶中位置正好是其在舊桶的位置加n // 這也正是為什么擴(kuò)容時(shí)容量在變成兩倍的原因 (鏈表遷移原理參考上面的圖) // ln 表示低位鏈表引用 // hn 表示高位鏈表引用 Node<K,V> ln, hn; // --------------------------------------------------------------- // CASE9: // 條件成立:表示當(dāng)前桶位是鏈表桶位 if (fh >= 0) { // 第一個(gè)元素的hash值大于等于0,說明該桶中元素是以鏈表形式存儲的 // 這里與HashMap遷移算法基本類似,唯一不同的是多了一步尋找lastRun // 這里的lastRun是提取出鏈表后面不用處理再特殊處理的子鏈表 // 比如所有元素的hash值與桶大小n與操作后的值分別為 0 0 4 4 0 0 0 // 則最后后面三個(gè)0對應(yīng)的元素肯定還是在同一個(gè)桶中 // 這時(shí)lastRun對應(yīng)的就是倒數(shù)第三個(gè)節(jié)點(diǎn) // 至于為啥要這樣處理,我也沒太搞明白 // lastRun機(jī)制: // 可以獲取出 當(dāng)前鏈表 末尾連續(xù)高位不變的 node int runBit = fh & n; Node<K,V> lastRun = f; for (Node<K,V> p = f.next; p != null; p = p.next) { int b = p.hash & n; if (b != runBit) { runBit = b; lastRun = p; } } // 看看最后這幾個(gè)元素歸屬于低位鏈表還是高位鏈表 // 條件成立:說明lastRun引用的鏈表為 低位鏈表,那么就讓 ln 指向 低位鏈表 if (runBit == 0) { ln = lastRun; hn = null; } // 否則,說明lastRun引用的鏈表為 高位鏈表,就讓 hn 指向 高位鏈表 else { hn = lastRun; ln = null; } // 遍歷鏈表,把hash&n為0的放在低位鏈表中,不為0的放在高位鏈表中 // 循環(huán)跳出條件:當(dāng)前循環(huán)結(jié)點(diǎn)!=lastRun for (Node<K,V> p = f; p != lastRun; p = p.next) { int ph = p.hash; K pk = p.key; V pv = p.val; if ((ph & n) == 0) ln = new Node<K,V>(ph, pk, pv, ln); else hn = new Node<K,V>(ph, pk, pv, hn); } // 低位鏈表的位置不變 setTabAt(nextTab, i, ln); // 高位鏈表的位置是:原位置 + n setTabAt(nextTab, i + n, hn); // 標(biāo)記當(dāng)前桶已遷移 setTabAt(tab, i, fwd); // advance為true,返回上面進(jìn)行--i操作 advance = true; } // --------------------------------------------------------------- // CASE10: // 條件成立:表示當(dāng)前桶位是 紅黑樹 代理結(jié)點(diǎn)TreeBin else if (f instanceof TreeBin) { // 如果第一個(gè)元素是樹節(jié)點(diǎn),也是一樣,分化成兩顆樹 // 也是根據(jù)hash&n為0放在低位樹中,不為0放在高位樹中 // 轉(zhuǎn)換頭結(jié)點(diǎn)為 treeBin引用 t TreeBin<K,V> t = (TreeBin<K,V>)f; // 低位雙向鏈表 lo 指向低位鏈表的頭 loTail 指向低位鏈表的尾巴 TreeNode<K,V> lo = null, loTail = null; // 高位雙向鏈表 lo 指向高位鏈表的頭 loTail 指向高位鏈表的尾巴 TreeNode<K,V> hi = null, hiTail = null; // lc 表示低位鏈表元素?cái)?shù)量 // hc 表示高位鏈表元素?cái)?shù)量 int lc = 0, hc = 0; // 遍歷整顆樹(TreeBin中的雙向鏈表,從頭結(jié)點(diǎn)至尾節(jié)點(diǎn)),根據(jù)hash&n是否為0分化成兩顆樹: for (Node<K,V> e = t.first; e != null; e = e.next) { // h 表示循環(huán)處理當(dāng)前元素的 hash int h = e.hash; // 使用當(dāng)前節(jié)點(diǎn) 構(gòu)建出來的新的TreeNode TreeNode<K,V> p = new TreeNode<K,V> (h, e.key, e.val, null, null); // --------------------------------------------------- // CASE11: // 條件成立:表示當(dāng)前循環(huán)節(jié)點(diǎn) 屬于低位鏈節(jié)點(diǎn) if ((h & n) == 0) { // 條件成立:說明當(dāng)前低位鏈表還沒有數(shù)據(jù) if ((p.prev = loTail) == null) lo = p; // 說明低位鏈表已經(jīng)有數(shù)據(jù)了,此時(shí)當(dāng)前元素追加到低位鏈表的末尾就行了 else loTail.next = p; // 將低位鏈表尾指針指向 p 節(jié)點(diǎn) loTail = p; ++lc; } // --------------------------------------------------- // CASE12: // 條件成立:當(dāng)前節(jié)點(diǎn)屬于高位鏈節(jié)點(diǎn) else { if ((p.prev = hiTail) == null) hi = p; else hiTail.next = p; hiTail = p; ++hc; } } // 如果分化的樹中元素個(gè)數(shù)小于等于6,則退化成鏈表 ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : (hc != 0) ? new TreeBin<K,V>(lo) : t; hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : (lc != 0) ? new TreeBin<K,V>(hi) : t; // 低位樹的位置不變 setTabAt(nextTab, i, ln); // 高位樹的位置是原位置加n setTabAt(nextTab, i + n, hn); // 標(biāo)記該桶已遷移 setTabAt(tab, i, fwd); // advance為true,返回上面進(jìn)行--i操作 advance = true; } } } } } }
補(bǔ)充,lastRun機(jī)制示意圖:
小節(jié):
(1)新桶數(shù)組大小是舊桶數(shù)組的兩倍;
(2)遷移元素先從靠后的桶開始;
(3)遷移完成的桶在里面放置一ForwardingNode類型的元素,標(biāo)記該桶遷移完成;
(4)遷移時(shí)根據(jù)hash&n是否等于0把桶中元素分化成兩個(gè)鏈表或樹;
(5)低位鏈表(樹)存儲在原來的位置;
(6)高們鏈表(樹)存儲在原來的位置加n的位置;
(7)遷移元素時(shí)會鎖住當(dāng)前桶,也是分段鎖的思想;
2、helpTransfer方法
helpTransfer
:協(xié)助擴(kuò)容(遷移元素),當(dāng)線程添加元素時(shí)發(fā)現(xiàn)table正在擴(kuò)容,且當(dāng)前元素所在的桶元素已經(jīng)遷移完成了,則協(xié)助遷移其它桶的元素。當(dāng)前桶元素遷移完成了才去協(xié)助遷移其它桶元素!
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) { // nextTab 引用的是 fwd.nextTable == map.nextTable 理論上是這樣。 // sc 保存map.sizeCtl Node<K,V>[] nextTab; int sc; // CASE0: 如果桶數(shù)組不為空,并且當(dāng)前桶第一個(gè)元素為ForwardingNode類型,并且nextTab不為空 // 說明當(dāng)前桶已經(jīng)遷移完畢了,才去幫忙遷移其它桶的元素 // 擴(kuò)容時(shí)會把舊桶的第一個(gè)元素置為ForwardingNode,并讓其nextTab指向新桶數(shù)組 // 條件一:tab != null 恒成立 true // 條件二:(f instanceof ForwardingNode) 恒成立 true // 條件三:((ForwardingNode<K,V>)f).nextTable) != null 恒成立 true if (tab != null && (f instanceof ForwardingNode) && (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { // 根據(jù)前表的長度tab.length去獲取擴(kuò)容唯一標(biāo)識戳,假設(shè) 16 -> 32 擴(kuò)容:1000 0000 0001 1011 int rs = resizeStamp(tab.length); // 條件一:nextTab == nextTable // 成立:表示當(dāng)前擴(kuò)容正在進(jìn)行中 // 不成立:1.nextTable被設(shè)置為Null了,擴(kuò)容完畢后,會被設(shè)為Null // 2.再次出發(fā)擴(kuò)容了...咱們拿到的nextTab 也已經(jīng)過期了... // 條件二:table == tab // 成立:說明 擴(kuò)容正在進(jìn)行中,還未完成 // 不成立:說明擴(kuò)容已經(jīng)結(jié)束了,擴(kuò)容結(jié)束之后,最后退出的線程 會設(shè)置 nextTable 為 table // 條件三:(sc = sizeCtl) < 0 // 成立:說明擴(kuò)容正在進(jìn)行中 // 不成立:說明sizeCtl當(dāng)前是一個(gè)大于0的數(shù),此時(shí)代表下次擴(kuò)容的閾值,當(dāng)前擴(kuò)容已經(jīng)結(jié)束。 while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) { // 條件一:(sc >>> RESIZE_STAMP_SHIFT) != rs // true -> 說明當(dāng)前線程獲取到的擴(kuò)容唯一標(biāo)識戳 非 本批次擴(kuò)容 // false -> 說明當(dāng)前線程獲取到的擴(kuò)容唯一標(biāo)識戳 是 本批次擴(kuò)容 // 條件二:JDK1.8 中有bug jira已經(jīng)提出來了 其實(shí)想表達(dá)的是 = sc == (rs << 16 ) + 1 // true -> 表示擴(kuò)容完畢,當(dāng)前線程不需要再參與進(jìn)來了 // false -> 擴(kuò)容還在進(jìn)行中,當(dāng)前線程可以參與 // 條件三:JDK1.8 中有bug jira已經(jīng)提出來了 其實(shí)想表達(dá)的是 = sc == (rs<<16) + MAX_RESIZERS // true -> 表示當(dāng)前參與并發(fā)擴(kuò)容的線程達(dá)到了最大值 65535 - 1 // false -> 表示當(dāng)前線程可以參與進(jìn)來 // 條件四:transferIndex <= 0 // true -> 說明map對象全局范圍內(nèi)的任務(wù)已經(jīng)分配完了,當(dāng)前線程進(jìn)去也沒活干.. // false -> 還有任務(wù)可以分配。 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || transferIndex <= 0) break; // 擴(kuò)容線程數(shù)加1 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { // 當(dāng)前線程幫忙遷移元素 transfer(tab, nextTab); break; } } return nextTab; } return table; }
3、總結(jié)
這塊代碼邏輯很難理解,可以結(jié)合著ConcurrentHashMap面試題去鞏固一下重新吸收~下一章是get、remove方法分析,也希望大家多多關(guān)注腳本之家的其他內(nèi)容!
相關(guān)文章
quartz實(shí)現(xiàn)定時(shí)功能實(shí)例詳解(servlet定時(shí)器配置方法)
Quartz是一個(gè)完全由java編寫的開源作業(yè)調(diào)度框架,下面提供一個(gè)小例子供大家參考,還有在servlet配置的方法2013-12-12IDEA導(dǎo)入eclipse項(xiàng)目并且部署到tomcat的步驟詳解
這篇文章主要給大家介紹了關(guān)于IDEA導(dǎo)入eclipse項(xiàng)目并且部署到tomcat的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02spring事務(wù)的propagation傳播屬性示例詳解
這篇文章主要為大家介紹了spring事務(wù)的propagation傳播屬性示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09java實(shí)現(xiàn)將數(shù)字轉(zhuǎn)換成人民幣大寫
前面給大家介紹過使用javascript,php,c#,python等語言實(shí)現(xiàn)人民幣大寫格式化,這篇文章主要介紹了java實(shí)現(xiàn)將數(shù)字轉(zhuǎn)換成人民幣大寫的代碼,非常的簡單實(shí)用,分享給大家,需要的朋友可以參考下2015-04-04Java Listener監(jiān)聽器使用規(guī)范詳細(xì)介紹
監(jiān)聽器是一個(gè)專門用于對其他對象身上發(fā)生的事件或狀態(tài)改變進(jìn)行監(jiān)聽和相應(yīng)處理的對象,當(dāng)被監(jiān)視的對象發(fā)生情況時(shí),立即采取相應(yīng)的行動(dòng)。監(jiān)聽器其實(shí)就是一個(gè)實(shí)現(xiàn)特定接口的普通java程序,這個(gè)程序?qū)iT用于監(jiān)聽另一個(gè)java對象的方法調(diào)用或?qū)傩愿淖?/div> 2023-01-01SpringMVC實(shí)現(xiàn)獲取請求參數(shù)方法詳解
Spring MVC 是 Spring 提供的一個(gè)基于 MVC 設(shè)計(jì)模式的輕量級 Web 開發(fā)框架,本質(zhì)上相當(dāng)于 Servlet,Spring MVC 角色劃分清晰,分工明細(xì),這篇文章主要介紹了SpringMVC實(shí)現(xiàn)獲取請求參數(shù)方法2022-09-09最新評論