主頁 > 軟體設計 > 多執行緒并發:以AQS中acquire()方法為例來分析多執行緒間的同步與協作

多執行緒并發:以AQS中acquire()方法為例來分析多執行緒間的同步與協作

2023-02-17 08:08:55 軟體設計

  談到java中的并發,我們就避不開執行緒之間的同步和協作問題,談到執行緒同步和協作我們就不能不談談jdk中提供的AbstractQueuedSynchronizer(翻譯過來就是抽象的佇列同步器)機制;

  (一)、AQS中的state和Node含義:

    AQS中提供了一個int volatile state狀態的變數用來標識共享資源,AQS定義了兩種資源的占用方式:

    1、獨占模式(EXCLUSIVE):表示同一個資源,在同一時刻只能被一個執行緒持有,例如ReentrantLock等;     2、共享模式(SHARED):表示同一個資源,在同一時刻可以被多個執行緒同時持有,例如Semaphore,CountDownLatch等;

    同時也提供了一個LCH佇列,用來存放獲取共享資源時候發生阻塞的Node節點,這個節點是對需要獲取資源執行緒的一個封裝,包含了執行緒本身和Node節點的狀態waitStatus,一共分為五種:

    /**表示當前節點中執行緒已經被取消調度,當timeout或者interrupt(假如會回應中斷的話)會觸發節點變更為此狀態,此節點的狀態再不會發生變化*/

    static final int CANCELLED = 1;

    /**表示當前節點中執行緒釋放資源后需要喚醒后繼節點執行緒,在采用尾插法將新結點加入到同步佇列的時候,會將新結點的前繼節點設定為SIGNAL */
    static final int SIGNAL = -1;
    /**表示當前節點中的執行緒在等待一個Condition喚醒,在其他執行緒中呼叫了這個Condition的signal()會將此Node從等待佇列的隊頭轉移到同步佇列的隊尾,嘗試競爭共享資源 */
    static final int CONDITION = -2;
    /**共享模式下,當前節點中的執行緒不僅需要喚醒后繼節點,還需要喚醒后繼節點的后繼節點*/
    static final int PROPAGATE = -3;

    除了上面這四種還有一個0,表示節點初始狀態,可以看出waitStatus<0才代表該節點是一個有效節點(即結點中的執行緒可以正常調度),

    AQS的設計其實是采用了模版方法的設計思想,在AbstractQueuedSynchronizer中這個頂層類中只提供了一些公共的方法實作如:同步佇列的維護等,而共享資源的獲取和釋放只提供了方法的定義,并不提供具體的實作(只是拋出了  unsupportedOperationException例外),通過這種方式就達到讓自定義的佇列同步器去強制實作的目的,

    上面我們提到,AQS定義了資源的兩種占用方式:獨占和共享,主要也就對應tryAcquire()-tryRelease(),tryAcquireShared()-tryReleaseShared()兩組方法需要我們自己去實作了:

????1、tryAcquire()獨占模式,嘗試獲取資源,成功為true,失敗為false;

????2、tryRelease()獨占模式,嘗試釋放資源,成功為true,失敗為false;

????3、tryAcquireShared()共享模式,嘗試獲取資源,負數為失敗,等于0為成功獲取,沒有剩余資源,大于0為成功獲取,有剩余共享資源;

????4、tryReleaseShared()共享模式,嘗試釋放資源,釋放之后需要喚醒等待節點為true,否則為false;

  (二)、代碼剖析:

  1、acquire(int)方法:

  此方法是獲取共享資源的入口方法,代碼如下:

    public final void acquire(int arg) {
        /*嘗試獲取共享資源,嘗試成功直接回傳*/
        if (!tryAcquire(arg)  
                /* 1、搶占共享資源失敗,則將當前節點放入到同步佇列的尾部,并標記為獨占模式;
                 * 2、使執行緒阻塞在同步佇列中獲取資源,直到獲取成功才回傳;如果整個程序中被中斷過就回傳true,否則就回傳false;*/
                && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
            /*阻塞獲取資源的程序中是不回應執行緒中斷的,內部進行了中斷檢測,檢測到了中斷請求,所以這塊進行執行緒中斷*/
            selfInterrupt();
        }
    }

  上面代碼的大概執行流程是:

  1、加塞搶占共享資源(因為同步佇列中可能還有其它節點等待),獲取成功則直接回傳;

  2、當前執行緒搶占共享資源失敗,呼叫addWaiter()將當前執行緒包裝成Node節點,加入同步佇列的尾部,并將當前節點標記為獨占模式(EXCLUSIVE),并回傳這個節點;

  3、當前執行緒呼叫acquireQueued()方法阻塞在同步佇列上獲取共享資源,該方法是一個同步方法,直到成功獲取了共享資源才會回傳,在這個阻塞獲取資源的程序中,如果檢測到發生了執行緒的中斷會回傳true,否則會回傳false;

  4、在第3步中阻塞獲取資源的程序中也不會回應中斷,所以在上個獲取共享程序中,檢測到了執行緒中斷標記會在acquireQueued()方法回傳為true時候,再呼叫selfInterrupt()中斷一次執行緒;

  2、addWaiter(Node mode)方法:

private Node addWaiter(Node mode) {
        /*以給定的模式將當前執行緒包裝成node節點*/
        Node node = new Node(Thread.currentThread(), mode);
        /*快速采用尾插法,將當前節點插入到同步佇列的隊尾*/
        Node predNode = tail;
        if (predNode != null) {
            /*preNode <-- node*/
            node.prev = predNode;
            /*采用CAS將node設定阻塞佇列的尾節點,設定成功,說明沒有并發*/
            if (compareAndSetTail(tail, node)) {
                predNode.next = node;
                /*尾插法插入成功,則直接回傳當前節點*/
                return node;
            }
        }
        /*"自旋"將節點加入到佇列的尾部,直到成功為止*/
        enq(node);
        return node;
    }

  此方法是當前執行緒首次搶占共享資源不成功,將當前執行緒以指定的模式(獨占或者共享)包裝成Node插入到同步佇列的尾節點的方法,其執行邏輯也在代碼中詳細注釋了,再概括下:

  1、將當前執行緒包裝為一個新的Node節點,并標記為獨占模式;

  2、如果當前同步佇列的尾節點不為空(表示當前佇列不為空),采用尾插法,將新的Node節點插入到同步佇列的尾部,考慮到多執行緒并發的安全問題采用了CAS方式設定同步佇列的尾節點,設定成功則就直接回傳這個新結點;

  3、如果快速插入不成功(可能有兩種情況:1、tail為空,即當前同步佇列為空;2、tail不為空,但是在使用CAS設定尾節點的時候出現了執行緒并發安全問題),則就呼叫enq(Node),采用“自旋”直到

將新結點成功插入到同步佇列的尾部;

  3、enq(Node) 方法:

 private Node enq(final Node node) {
        /*"自旋",將給定的節點插入到同步佇列的尾部*/
        for (;;) {
            Node t = tail;
            /*同步佇列為空,則創建一個thread為null的Node節點作為同步佇列的頭結點,并且將尾節點也設定為頭結點*/
            if (t == null) {
                /*CAS操作,設定同步佇列的頭結點*/
                if (compareAndSetHead(new Node())) {
                    /*將尾節點設定為頭結點,進入下次"自旋"*/
                    tail = head;
                }
            }else {
                /*尾部節點不為空,則進行正常添加動作*/
                node.prev = t;
                /*CAS操作,設定同步佇列的頭結點*/
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

  此方法采用了“自旋”操作,并結合CAS操作,在保證多執行緒并發執行緒安全的前提下,一定可以安全地將這個新結點插入到同步佇列的尾部,

  此段代碼的執行流程是:

  1、判斷這個同步佇列是否為空(判斷tail為空即可),為空則創建一個空節點(不包含任何執行緒),并向尾節點也指向頭結點,

  2、然后進行下一次的自旋嘗試CAS操作,將方法傳入的Node節點嘗試加入到佇列的尾部,也通過一定次數的自旋操作,一定會加入到同步佇列的尾部,然后退出;

  3、如果一開始進入這個方法,佇列不為空,則就執行第2步驟不斷嘗試,直到成功;

  到此為止addWaiter()方法中的邏輯就分析完了,執行完畢之后回傳,就行呼叫acquireQueued(Node)方法阻塞此執行緒,等待獲取資源了,

  acquireQueued(Node,int)方法:

final boolean acquireQueued(final Node node, int arg) {
        /*標記阻塞獲取資源的程序中是否發生了異常*/
        boolean failed = true;
        
        try {
            /*標記執行緒阻塞的程序中是否發生了中斷*/
            boolean interrupted = false;
            /*執行緒自旋阻塞*/
            for(;;){
          
/*獲取當前節點的前驅結點*/ final Node p = node.predecessor(); /*1、前驅結點是頭結點,則說明當前執行緒有資格獲取共享資源,嘗試獲取,獲取成功,將當前節點設定為頭結點*/ if (p == head && tryAcquire(arg)) { /*將當前節點設定為頭結點*/ setHead(node); p.next = null; //help GC failed = false;
            /*回傳執行緒在阻塞的程序中是否接受到了中斷請求*/
return interrupted; } /*2、3當前節點的前驅結點不是頭結點,判斷當前執行緒是否可以掛起*/ if (shouldParkAfterFailedAcquire(p, node) /*4、當前執行緒可以掛起,則掛起執行緒,并且執行緒被unpark()或者interrupt()喚醒,檢查執行緒的狀態*/ && parkAndCheckInterrupt()) { interrupted = true; } } }finally{
  
if (failed) { /*5、阻塞獲取同步資源的時候,發生了例外,將當前Node節點從同步佇列中出隊*/ cancelAcquire(node); } }

  acquireQueued(Node)這個方法的目的就是使得當前執行緒阻塞,等待獲取資源,獲取成功之后才回傳,那么如何阻塞呢?看了上面的代碼,讀到這里我想大家心里都有了答案:要么自旋,要么主動park(),此方法中采用了這兩種方式結合的方法,其大致的執行流程如下:

  1、自旋,首先當前節點的前繼節點是頭結點(走到這里來了,說明前繼節點正在占用共享資源,有可能在這個程序中正好釋放了),那么我們當前執行緒就有資格去嘗試獲取共享資源,如果獲取共享資源成功,則結束自旋阻塞;

  2、如果前繼節點不是頭結點,或者爭搶共享資源失敗,那么我們呼叫shouldParkAfterFailedAcquire(Node,Node)方法,判斷是否可以將此執行緒暫時掛起(不能無限制地自旋,會造成CPU占用率飆升,安全的做法是自旋找到一個合適的點,將當前執行緒park()阻塞掛起);

  3、當前掛起之前,要向前尋找能將它喚醒的前繼節點,待前繼節點釋放資源之后,unpark()喚醒當前被阻塞的節點;

  4、執行緒已經到達安全點,可以呼叫parkAndCheckInterrupt()阻塞并等待喚醒,喚醒之后再檢查下,阻塞的這個程序中是否發生了執行緒中斷請求,由于這個阻塞程序是不回應執行緒中斷的,所以需要將這個中斷請求的狀態傳播出去;

  5、阻塞獲取同步資源的時候,發生了例外,取消當前執行緒的在同步佇列中的排隊;

  shouldParkAfterFailedAcquire(Node, Node )方法:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        /*判斷前驅結點的狀態,只有前驅結點的狀態為SIGNAL,后繼節點才能被喚醒,所以其可以安心地掛起來了*/
        if (ws == node.waitStatus) {
            return true;
        }
        
        /*ws>0表示前驅結點中的執行緒已經被取消調度了,則認為其是無效節點,繼續向前查找,直至找到有效狀態的節點*/
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        }else {
            /*前驅結點狀態正常,將前驅結點狀態設定為SIGNAL,則前驅結點釋放資源的時候,就可以嘗試喚醒當前這個后繼節點了*/
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

   這個方法是在自旋中用來判斷是否可以將執行緒安全地park(),阻塞自旋的,那么何時才能將當前執行緒安全地掛起呢?回想下,我們之前提到的節點的五種狀態中有一種SIGNAL,表示當前節點的執行緒釋放資源,可以喚醒后繼節點,所以我們就在執行緒掛起之前找到它的有效的前繼結點,將它的waitStatus狀態設定為SIGNAL,就可以保證前繼節點釋放資源之后,當前節點中的執行緒就可以被及時地喚醒,結束阻塞了,當前執行緒掛起之前的準備作業都做完了,那么接下來就需要呼叫parkAndCheckInterrupt()方法,進行執行緒的掛起了,

  parkAndCheckInterrupt()方法:

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

  執行到這里了,說明執行緒可以阻塞,呼叫park()方法阻塞執行緒,等待其他執行緒中unpark()或者interrupt()喚醒次執行緒,喚醒之后執行Thread.interrupted()方法,檢測阻塞程序中的執行緒請求中斷,進入下次自旋,嘗試獲取共享資源,如果在阻塞獲取資源的程序中,發生了例外,failed = true,則執行finally中cancelAcquire(Node)方法,取消當前節點中執行緒的調度,

  上面說了分析了這么多,只說了執行緒間的獲取資源時候的同步問題,那么執行緒間的協作在哪里體現呢?答案就是在acquireQueued(Node,int)方法中的finally塊中,執行緒在阻塞獲取共享資源的時候發生了例外,就會執行此方法將此節點從同步佇列中出隊,下面我們來分析下cancelAcquire(Node)方法:

  cancelAcquire(Node)方法:

 1     private void cancelAcquire(Node node) {
 2         //當前節點為空,則說明當前執行緒永遠不會被調度到了,所以直接回傳
 3         if (node == null) {
 4             return;
 5         }
 6         
 7         /**
 8          * 接下來將點前Node節點從同步佇列出隊,主要做以下幾件事:
 9          * 1、將當前節點不與任何執行緒系結,設定當前節點為Node.CANCELLED狀態;
10          * 2、將當前取消節點的前置非取消節點和后置非取消節點"鏈接"起來;
11          * 3、如果前置節點釋放了鎖,那么當前取消節點承擔起后續節點的喚醒職責,
12          */
13         
14         //1、取消當前節點與執行緒的系結
15         node.thread = null;
16         
17         //2、找到當前節點的有效前繼節點pred
18         Node pred = node.prev;
19         while (pred.waitStatus > 0) {
20             //為什么雙向鏈表從后往前遍歷呢?而不是從前往后遍歷呢?
21             node.prev = pred = pred.prev;
22         }
23         //用作CAS操作時候的條件判斷需要使用的值
24         Node predNext = pred.next;
25         
26         //3、將當前節點設定為取消狀態
27         node.waitStatus = Node.CANCELLED;
28         
29         /**
30          * 接下來就需要將當前取消節點的前后兩個有效節點"鏈接"起來了,"達成讓當前node節點出隊的目的",
31          * 這里按照node節點在同步佇列中的不同位置分了三種情況:
32          * 1、node節點是同步佇列的尾節點tail;
33          * 2、node節點既不是同步佇列頭結點head的后繼節點,也不是尾節點tail;
34          * 3、node節點是同步佇列頭結點head的后繼節點;
35          */
36         
37         //1、node是尾節點,并且執行程序中沒有并發,直接將pred設定為同步佇列的tail
38         if (node == tail && compareAndSetTail(node, pred)) {
39             /*
40              * 此時pred已經設定為同步佇列的tail,需要通過CAS操作,將pred的next指向null,沒有節點再參考node,就完成了node節點的出隊42              */
43             compareAndSetNext(pred, predNext, null);
44         }else {
45             /*
46              * 2、node不是尾節點,也不是頭結點head的后繼節點,那么當前節點node出隊以后,node的有效前繼結點pred,
47              *  就有義務在它自身釋放資源的時候,喚醒node的有效后繼節點successor,即將pred的狀態設定為Node.SIGNAL;
48              */
49             int ws;
50             //能執行到這里,說明當前node節點不是head的后繼節點,也不是同步佇列tail節點
51             if (pred != head &&
52                     ((ws = pred.waitStatus) == Node.SIGNAL ||
53                     //前繼節點狀態雖然有效但不是SIGNAL,采用CAS操作設定為SIGNAL確保后繼有效節點可以被喚醒
54                     (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && 
55                     pred.thread != null) {
56                 Node next = node.next;
57                 //只負責喚醒有效后繼節點
58                 if (next != null && next.waitStatus <= 0) {
59                     /**
60                      * 下面這段代碼相當于將pred-->next,我們提到這個同步佇列是個雙向佇列,那么pred<--next這是誰執行的呢?
61                      * 答案是其他執行緒:其它執行緒在后序的獲取共享資源在同步佇列中阻塞的時候,呼叫shouldParkAfterFailedAcquire()方法,
62                      * 從后向前遍歷佇列,尋找能喚醒它的有效前繼節點,當找到node的時候,因為它的狀態已經是Node.CANCELLED,所以會忽略node節點,
63                      * 直到遍歷到有效前繼節點pred,將next.prev執行pred,即next--->pred,沒有節點再參考node節點,所以node節點至此才完成出隊,
64                      */
65                     compareAndSetNext(pred, predNext, next);
66                 }
67             }else {
68                 //3、說明node節點是同步佇列head的后繼節點,呼叫unparkSuccessor(Node)喚醒其他執行緒,達到讓當前node"出隊",
69                 unparkSuccessor(node);
70             }
71             
72             node.next = node;//help GC
73         }
74     }

   這個方法的中的注釋寫的很詳細了,執行緒間的協作主要體現在第65行的代碼中,原因也在注釋中寫明了,就不再贅述了,

  還有一個非常關鍵的問題就是:為什么我們在遍歷同步佇列的時候是從尾部向前遍歷,而不是從頭部向尾部遍歷呢?我們可以回過頭去看看入隊時候的enq(Node)方法:

  

  關鍵在11行-14行這部分代碼中,11行保證了多執行緒環境下,采用自旋可以將當前執行緒順利地加入到同步佇列的尾部,
  假如有執行緒A執行了滿足了if條件,成功將執行緒A放入了tail節點,還未執行到12行,t.next = null,此時發生了執行緒切換執行B執行緒,B執行緒也執行了此方法,并且執行完畢,尾插法會將B執行緒節點追加到A執行緒節點之后,這時候又有個C執行緒執行了遍歷操作,假設從隊頭向隊尾遍歷,遍歷到A節點時,那么可能會出現t.next = null這種情況,停止遍歷,漏掉B執行緒節點的情況,而采用從同步佇列的尾部向頭部遍歷則可以避免這個問題,

  下個問題就是unparkSuccessor(node)方法的原理是什么呢?

  unparkSuccessor(node)方法:

 1   private void unparkSuccessor(Node node) {
 2         /*
 3          * If status is negative (i.e., possibly needing signal) try
 4          * to clear in anticipation of signalling.  It is OK if this
 5          * fails or if status is changed by waiting thread.
 6          */
 7         //在這里,這個節點其實是同步佇列的頭結點,頭結點喚醒后繼節點之后,使命就完成了,所以應該將其狀態置為0
 8         int ws = node.waitStatus;
 9         if (ws < 0)
10             compareAndSetWaitStatus(node, ws, 0);
11 
12         /*
13          * Thread to unpark is held in successor, which is normally
14          * just the next node.  But if cancelled or apparently null,
15          * traverse backwards from tail to find the actual
16          * non-cancelled successor.
17          */
18         Node s = node.next;
19         //因為s.next相當于從同步佇列的頭部遍歷所以可能會出現s == null的情況,上面分析過原因,不再贅述了,
20         if (s == null || s.waitStatus > 0) {
21             s = null;
22             //從同步佇列的尾部向前遍歷,找到當前node節點(頭結點)的最近的有效后繼節點
23             for (Node t = tail; t != null && t != node; t = t.prev)
24                 if (t.waitStatus <= 0)
25                     s = t;
26         }
27         
28         /**
29          * 找到最近的有效后繼節點,則喚醒后繼節點中的執行緒在parkAndCheckInterrupt()方法上的阻塞,去嘗試競爭共享資源,
30          * 這就體現了執行緒之間的協作,而在這個競爭的程序中也會忽略這個Node.CANCELLED狀態的節點,這當前node節點也就放棄了競爭共享資源的機會,相當于出隊了,
31          */
32         if (s != null)
33             LockSupport.unpark(s.thread);
34     }

   以上就是對unparksuccessor(Node)方法的簡單分析了,再回過頭來,我們在cancelAcquire(Node)將當前要取消Node按照位置關系分為了三種,為什么我們會忽略head位置呢?

     從setHead()的實作以及所有呼叫的地方可以看出,head指向的節點必定是拿到鎖(或是競爭資源)的節點,而head的后繼節點則是有資格爭奪鎖的節點,我們不在需要甚至喚醒條件了,再后續的節點,就是阻塞著的了,head指向的節點,曾經關聯的執行緒必定已經獲取到資源,在執行了,所以head無需再關聯到該執行緒了,head所指向的節點,也無需再參與任何的競爭操作了,現在再來看node出隊時的分類,就好理解了,head既然不會參與任何資源競爭了,自然也就和cancelAquire()無關了,

  仔細分析這個acquire()方法流程非常復雜,找了個一張網上一個博主畫的流程圖非常棒,這里借鑒一下:

  

  好了文章就寫到這里了,鑒于水平有限,有說的不對的地方歡迎大家批評指正,

  參考文章地址:

  1、https://blog.csdn.net/weixin_38106322/article/details/107121149

  2、https://www.jianshu.com/p/01f2046aab64

  3、https://blog.csdn.net/foxException/article/details/108917338

本文來自博客園,作者:一只烤鴨朝北走,僅用于技術學習,所有資源都來源于網路,部分是轉發,部分是個人總結,歡迎共同學習和轉載,轉載請在醒目位置標明原文,如有侵權,請留言告知,及時撤除,轉載請注明原文鏈接:https://www.cnblogs.com/wha6239/p/17122948.html

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

標籤:設計模式

上一篇:建造者模式

下一篇:如何從0開始搭建 Vue 組件庫

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