深入理解AbstractQueuedSynchronizer(AQS)

1. AQS簡介

在上一篇文章中我們對lock和AbstractQueuedSynchronizer(AQS)有了初步的認識。在同步組件的實現中,AQS是核心部分,同步組件的實現者通過使用AQS提供的模板方法實現同步組件語義,AQS則實現了對同步狀態的管理,以及對阻塞線程進行排隊,等待通知等等一些底層的實現處理。AQS的核心也包括了這些方面:同步隊列,獨占式鎖的獲取和釋放,共享鎖的獲取和釋放以及可中斷鎖,超時等待鎖獲取這些特性的實現,而這些實際上則是AQS提供出來的模板方法,歸納整理如下:

獨占式鎖:

void acquire(int arg):獨占式獲取同步狀態,如果獲取失敗則插入同步隊列進行等待; void acquireInterruptibly(int arg):與acquire方法相同,但在同步隊列中進行等待的時候可以檢測中斷; boolean tryAcquireNanos(int arg, long nanosTimeout):在acquireInterruptibly基礎上增加了超時等待功能,在超時時間內沒有獲得同步狀態返回false; boolean release(int arg):釋放同步狀態,該方法會喚醒在同步隊列中的下一個節點

共享式鎖:

void acquireShared(int arg):共享式獲取同步狀態,與獨占式的區別在于同一時刻有多個線程獲取同步狀態; void acquireSharedInterruptibly(int arg):在acquireShared方法基礎上增加了能響應中斷的功能; boolean tryAcquireSharedNanos(int arg, long nanosTimeout):在acquireSharedInterruptibly基礎上增加了超時等待的功能; boolean releaseShared(int arg):共享式釋放同步狀態

要想掌握AQS的底層實現,其實也就是對這些模板方法的邏輯進行學習。在學習這些模板方法之前,我們得首先了解下AQS中的同步隊列是一種什么樣的數據結構,因為同步隊列是AQS對同步狀態的管理的基石。

2. 同步隊列

當共享資源被某個線程占有,其他請求該資源的線程將會阻塞,從而進入同步隊列。就數據結構而言,隊列的實現方式無外乎兩者一是通過數組的形式,另外一種則是鏈表的形式。AQS中的同步隊列則是通過鏈式方式進行實現。接下來,很顯然我們至少會抱有這樣的疑問:**1. 節點的數據結構是什么樣的?2. 是單向還是雙向?3. 是帶頭結點的還是不帶頭節點的?**我們依舊先是通過看源碼的方式。

在AQS有一個靜態內部類Node,其中有這樣一些屬性:

volatile int waitStatus //節點狀態 volatile Node prev //當前節點/線程的前驅節點 volatile Node next; //當前節點/線程的后繼節點 volatile Thread thread;//加入同步隊列的線程引用 Node nextWaiter;//等待隊列中的下一個節點

節點的狀態有以下這些:

int CANCELLED = 1//節點從同步隊列中取消 int SIGNAL = -1//后繼節點的線程處于等待狀態,如果當前節點釋放同步狀態會通知后繼節點,使得后繼節點的線程能夠運行; int CONDITION = -2//當前節點進入等待隊列中 int PROPAGATE = -3//表示下一次共享式同步狀態獲取將會無條件傳播下去 int INITIAL = 0;//初始狀態

現在我們知道了節點的數據結構類型,并且每個節點擁有其前驅和后繼節點,很顯然這是一個雙向隊列。同樣的我們可以用一段demo看一下。

public class LockDemo {private static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {for (int i = 0; i < 5; i++) {Thread thread = new Thread(() -> {lock.lock();try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}});thread.start();}}
}
復制代碼

實例代碼中開啟了5個線程,先獲取鎖之后再睡眠10S中,實際上這里讓線程睡眠是想模擬出當線程無法獲取鎖時進入同步隊列的情況。通過debug,當Thread-4(在本例中最后一個線程)獲取鎖失敗后進入同步時,AQS時現在的同步隊列如圖所示:

Thread-0先獲得鎖后進行睡眠,其他線程(Thread-1,Thread-2,Thread-3,Thread-4)獲取鎖失敗進入同步隊列,同時也可以很清楚的看出來每個節點有兩個域:prev(前驅)和next(后繼),并且每個節點用來保存獲取同步狀態失敗的線程引用以及等待狀態等信息。另外AQS中有兩個重要的成員變量:

private transient volatile Node head;
private transient volatile Node tail;
復制代碼

也就是說AQS實際上通過頭尾指針來管理同步隊列,同時實現包括獲取鎖失敗的線程進行入隊,釋放鎖時對同步隊列中的線程進行通知等核心方法。其示意圖如下:

通過對源碼的理解以及做實驗的方式,現在我們可以清楚的知道這樣幾點:

  1. 節點的數據結構,即AQS的靜態內部類Node,節點的等待狀態等信息
  2. 同步隊列是一個雙向隊列,AQS通過持有頭尾指針管理同步隊列

那么,節點如何進行入隊和出隊是怎樣做的了?實際上這對應著鎖的獲取和釋放兩個操作:獲取鎖失敗進行入隊操作,獲取鎖成功進行出隊操作。

3. 獨占鎖

3.1 獨占鎖的獲取(acquire方法)

我們繼續通過看源碼和debug的方式來看,還是以上面的demo為例,調用lock()方法是獲取獨占式鎖,獲取失敗就將當前線程加入同步隊列,成功則線程執行。而lock()方法實際上會調用AQS的**acquire()**方法,源碼如下

public final void acquire(int arg) {//先看同步狀態是否獲取成功,如果成功則方法結束返回//若失敗則先調用addWaiter()方法再調用acquireQueued()方法if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}
復制代碼

關鍵信息請看注釋,acquire根據當前獲得同步狀態成功與否做了兩件事情:1. 成功,則方法結束返回,2. 失敗,則先調用addWaiter()然后在調用acquireQueued()方法。

獲取同步狀態失敗,入隊操作

當線程獲取獨占式鎖失敗后就會將當前線程加入同步隊列,那么加入隊列的方式是怎樣的了?我們接下來就應該去研究一下addWaiter()和acquireQueued()。addWaiter()源碼如下:

private Node addWaiter(Node mode) {// 1. 將當前線程構建成Node類型Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failure// 2. 當前尾節點是否為null?Node pred = tail;if (pred != null) {// 2.2 將當前節點尾插入的方式插入同步隊列中node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 2.1. 當前同步隊列尾節點為null,說明當前線程是第一個加入同步隊列進行等待的線程enq(node);return node;
}
復制代碼

分析可以看上面的注釋。程序的邏輯主要分為兩個部分:**1. 當前同步隊列的尾節點為null,調用方法enq()插入;2. 當前隊列的尾節點不為null,則采用尾插入(compareAndSetTail()方法)的方式入隊。**另外還會有另外一個問題:如果 if (compareAndSetTail(pred, node))為false怎么辦?會繼續執行到enq()方法,同時很明顯compareAndSetTail是一個CAS操作,通常來說如果CAS操作失敗會繼續自旋(死循環)進行重試。因此,經過我們這樣的分析,enq()方法可能承擔兩個任務:**1. 處理當前同步隊列尾節點為null時進行入隊操作;2. 如果CAS尾插入節點失敗后負責自旋進行嘗試。**那么是不是真的就像我們分析的一樣了?只有源碼會告訴我們答案:),enq()源碼如下:

private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initialize//1. 構造頭結點if (compareAndSetHead(new Node()))tail = head;} else {// 2. 尾插入,CAS操作失敗自旋嘗試node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}
復制代碼

在上面的分析中我們可以看出在第1步中會先創建頭結點,說明同步隊列是帶頭結點的鏈式存儲結構。帶頭結點與不帶頭結點相比,會在入隊和出隊的操作中獲得更大的便捷性,因此同步隊列選擇了帶頭結點的鏈式存儲結構。那么帶頭節點的隊列初始化時機是什么?自然而然是在tail為null時,即當前線程是第一次插入同步隊列。compareAndSetTail(t, node)方法會利用CAS操作設置尾節點,如果CAS操作失敗會在for (;;)for死循環中不斷嘗試,直至成功return返回為止。因此,對enq()方法可以做這樣的總結:

  1. 在當前線程是第一個加入同步隊列時,調用compareAndSetHead(new Node())方法,完成鏈式隊列的頭結點的初始化
  2. 自旋不斷嘗試CAS尾插入節點直至成功為止

現在我們已經很清楚獲取獨占式鎖失敗的線程包裝成Node然后插入同步隊列的過程了?那么緊接著會有下一個問題?在同步隊列中的節點(線程)會做什么事情了來保證自己能夠有機會獲得獨占式鎖了?帶著這樣的問題我們就來看看acquireQueued()方法,從方法名就可以很清楚,這個方法的作用就是排隊獲取鎖的過程,源碼如下:

final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {// 1. 獲得當前節點的先驅節點final Node p = node.predecessor();// 2. 當前節點能否獲取獨占式鎖					// 2.1 如果當前節點的先驅節點是頭結點并且成功獲取同步狀態,即可以獲得獨占式鎖if (p == head && tryAcquire(arg)) {//隊列頭指針用指向當前節點setHead(node);//釋放前驅節點p.next = null; // help GCfailed = false;return interrupted;}// 2.2 獲取鎖失敗,線程進入等待狀態等待獲取獨占式鎖if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}
復制代碼

程序邏輯通過注釋已經標出,整體來看這是一個這又是一個自旋的過程(for (;;)),代碼首先獲取當前節點的先驅節點,如果先驅節點是頭結點的并且成功獲得同步狀態的時候(if (p == head && tryAcquire(arg))),當前節點所指向的線程能夠獲取鎖。反之,獲取鎖失敗進入等待狀態。整體示意圖為下圖:

獲取鎖成功,出隊操作

獲取鎖的節點出隊的邏輯是:

//隊列頭結點引用指向當前節點
setHead(node);
//釋放前驅節點
p.next = null; // help GC
failed = false;
return interrupted;
復制代碼

setHead()方法為:

private void setHead(Node node) {head = node;node.thread = null;node.prev = null;
}
復制代碼

將當前節點通過setHead()方法設置為隊列的頭結點,然后將之前的頭結點的next域設置為null并且pre域也為null,即與隊列斷開,無任何引用方便GC時能夠將內存進行回收。示意圖如下:

那么當獲取鎖失敗的時候會調用shouldParkAfterFailedAcquire()方法和parkAndCheckInterrupt()方法,看看他們做了什么事情。shouldParkAfterFailedAcquire()方法源碼為:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL)/** This node has already set status asking a release* to signal it, so it can safely park.*/return true;if (ws > 0) {/** Predecessor was cancelled. Skip over predecessors and* indicate retry.*/do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {/** waitStatus must be 0 or PROPAGATE.  Indicate that we* need a signal, but don't park yet.  Caller will need to* retry to make sure it cannot acquire before parking.*/compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}
復制代碼

shouldParkAfterFailedAcquire()方法主要邏輯是使用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)使用CAS將節點狀態由INITIAL設置成SIGNAL,表示當前線程阻塞。當compareAndSetWaitStatus設置失敗則說明shouldParkAfterFailedAcquire方法返回false,然后會在acquireQueued()方法中for (;;)死循環中會繼續重試,直至compareAndSetWaitStatus設置節點狀態位為SIGNAL時shouldParkAfterFailedAcquire返回true時才會執行方法parkAndCheckInterrupt()方法,該方法的源碼為:

private final boolean parkAndCheckInterrupt() {//使得該線程阻塞LockSupport.park(this);return Thread.interrupted();
}
復制代碼

該方法的關鍵是會調用LookSupport.park()方法(關于LookSupport會在以后的文章進行討論),該方法是用來阻塞當前線程的。因此到這里就應該清楚了,acquireQueued()在自旋過程中主要完成了兩件事情:

  1. 如果當前節點的前驅節點是頭節點,并且能夠獲得同步狀態的話,當前線程能夠獲得鎖該方法執行結束退出
  2. 獲取鎖失敗的話,先將節點狀態設置成SIGNAL,然后調用LookSupport.park方法使得當前線程阻塞

經過上面的分析,獨占式鎖的獲取過程也就是acquire()方法的執行流程如下圖所示:

3.2 獨占鎖的釋放(release()方法)

獨占鎖的釋放就相對來說比較容易理解了,廢話不多說先來看下源碼:

public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;
}
復制代碼

這段代碼邏輯就比較容易理解了,如果同步狀態釋放成功(tryRelease返回true)則會執行if塊中的代碼,當head指向的頭結點不為null,并且該節點的狀態值不為0的話才會執行unparkSuccessor()方法。unparkSuccessor方法源碼:

private void unparkSuccessor(Node node) {/** If status is negative (i.e., possibly needing signal) try* to clear in anticipation of signalling.  It is OK if this* fails or if status is changed by waiting thread.*/int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);/** Thread to unpark is held in successor, which is normally* just the next node.  But if cancelled or apparently null,* traverse backwards from tail to find the actual* non-cancelled successor.*///頭節點的后繼節點Node s = node.next;if (s == null || s.waitStatus > 0) {s = null;for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null)//后繼節點不為null時喚醒該線程LockSupport.unpark(s.thread);
}
復制代碼

源碼的關鍵信息請看注釋,首先獲取頭節點的后繼節點,當后繼節點的時候會調用LookSupport.unpark()方法,該方法會喚醒該節點的后繼節點所包裝的線程。因此,每一次鎖釋放后就會喚醒隊列中該節點的后繼節點所引用的線程,從而進一步可以佐證獲得鎖的過程是一個FIFO(先進先出)的過程。

到現在我們終于啃下了一塊硬骨頭了,通過學習源碼的方式非常深刻的學習到了獨占式鎖的獲取和釋放的過程以及同步隊列。可以做一下總結:

  1. 線程獲取鎖失敗,線程被封裝成Node進行入隊操作,核心方法在于addWaiter()和enq(),同時enq()完成對同步隊列的頭結點初始化工作以及CAS操作失敗的重試;
  2. 線程獲取鎖是一個自旋的過程,當且僅當 當前節點的前驅節點是頭結點并且成功獲得同步狀態時,節點出隊即該節點引用的線程獲得鎖,否則,當不滿足條件時就會調用LookSupport.park()方法使得線程阻塞
  3. 釋放鎖的時候會喚醒后繼節點;

總體來說:在獲取同步狀態時,AQS維護一個同步隊列,獲取同步狀態失敗的線程會加入到隊列中進行自旋;移除隊列(或停止自旋)的條件是前驅節點是頭結點并且成功獲得了同步狀態。在釋放同步狀態時,同步器會調用unparkSuccessor()方法喚醒后繼節點。

獨占鎖特性學習

3.3 可中斷式獲取鎖(acquireInterruptibly方法)

我們知道lock相較于synchronized有一些更方便的特性,比如能響應中斷以及超時等待等特性,現在我們依舊采用通過學習源碼的方式來看看能夠響應中斷是怎么實現的。可響應中斷式鎖可調用方法lock.lockInterruptibly();而該方法其底層會調用AQS的acquireInterruptibly方法,源碼為:

public final void acquireInterruptibly(int arg)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();if (!tryAcquire(arg))//線程獲取鎖失敗doAcquireInterruptibly(arg);
}
復制代碼

在獲取同步狀態失敗后就會調用doAcquireInterruptibly方法:

private void doAcquireInterruptibly(int arg)throws InterruptedException {//將節點插入到同步隊列中final Node node = addWaiter(Node.EXCLUSIVE);boolean failed = true;try {for (;;) {final Node p = node.predecessor();//獲取鎖出隊if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())//線程中斷拋異常throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}
}
復制代碼

關鍵信息請看注釋,現在看這段代碼就很輕松了吧:),與acquire方法邏輯幾乎一致,唯一的區別是當parkAndCheckInterrupt返回true時即線程阻塞時該線程被中斷,代碼拋出被中斷異常。

3.4 超時等待式獲取鎖(tryAcquireNanos()方法)

通過調用lock.tryLock(timeout,TimeUnit)方式達到超時等待獲取鎖的效果,該方法會在三種情況下才會返回:

  1. 在超時時間內,當前線程成功獲取了鎖;
  2. 當前線程在超時時間內被中斷;
  3. 超時時間結束,仍未獲得鎖返回false。

我們仍然通過采取閱讀源碼的方式來學習底層具體是怎么實現的,該方法會調用AQS的方法tryAcquireNanos(),源碼為:

public final boolean tryAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();return tryAcquire(arg) ||//實現超時等待的效果doAcquireNanos(arg, nanosTimeout);
}
復制代碼

很顯然這段源碼最終是靠doAcquireNanos方法實現超時等待的效果,該方法源碼如下:

private boolean doAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {if (nanosTimeout <= 0L)return false;//1. 根據超時時間和當前時間計算出截止時間final long deadline = System.nanoTime() + nanosTimeout;final Node node = addWaiter(Node.EXCLUSIVE);boolean failed = true;try {for (;;) {final Node p = node.predecessor();//2. 當前線程獲得鎖出隊列if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return true;}// 3.1 重新計算超時時間nanosTimeout = deadline - System.nanoTime();// 3.2 已經超時返回falseif (nanosTimeout <= 0L)return false;// 3.3 線程阻塞等待 if (shouldParkAfterFailedAcquire(p, node) &&nanosTimeout > spinForTimeoutThreshold)LockSupport.parkNanos(this, nanosTimeout);// 3.4 線程被中斷拋出被中斷異常if (Thread.interrupted())throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}
}
復制代碼

程序邏輯如圖所示:

程序邏輯同獨占鎖可響應中斷式獲取基本一致,唯一的不同在于獲取鎖失敗后,對超時時間的處理上,在第1步會先計算出按照現在時間和超時時間計算出理論上的截止時間,比如當前時間是8h10min,超時時間是10min,那么根據deadline = System.nanoTime() + nanosTimeout計算出剛好達到超時時間時的系統時間就是8h 10min+10min = 8h 20min。然后根據deadline - System.nanoTime()就可以判斷是否已經超時了,比如,當前系統時間是8h 30min很明顯已經超過了理論上的系統時間8h 20min,deadline - System.nanoTime()計算出來就是一個負數,自然而然會在3.2步中的If判斷之間返回false。如果還沒有超時即3.2步中的if判斷為true時就會繼續執行3.3步通過LockSupport.parkNanos使得當前線程阻塞,同時在3.4步增加了對中斷的檢測,若檢測出被中斷直接拋出被中斷異常。

4. 共享鎖

4.1 共享鎖的獲取(acquireShared()方法)

在聊完AQS對獨占鎖的實現后,我們繼續一鼓作氣的來看看共享鎖是怎樣實現的?共享鎖的獲取方法為acquireShared,源碼為:

public final void acquireShared(int arg) {if (tryAcquireShared(arg) < 0)doAcquireShared(arg);
}
復制代碼

這段源碼的邏輯很容易理解,在該方法中會首先調用tryAcquireShared方法,tryAcquireShared返回值是一個int類型,當返回值為大于等于0的時候方法結束說明獲得成功獲取鎖,否則,表明獲取同步狀態失敗即所引用的線程獲取鎖失敗,會執行doAcquireShared方法,該方法的源碼為:

private void doAcquireShared(int arg) {final Node node = addWaiter(Node.SHARED);boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();if (p == head) {int r = tryAcquireShared(arg);if (r >= 0) {// 當該節點的前驅節點是頭結點且成功獲取同步狀態setHeadAndPropagate(node, r);p.next = null; // help GCif (interrupted)selfInterrupt();failed = false;return;}}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}
復制代碼

現在來看這段代碼會不會很容易了?邏輯幾乎和獨占式鎖的獲取一模一樣,這里的自旋過程中能夠退出的條件是當前節點的前驅節點是頭結點并且tryAcquireShared(arg)返回值大于等于0即能成功獲得同步狀態

4.2 共享鎖的釋放(releaseShared()方法)

共享鎖的釋放在AQS中會調用方法releaseShared:

public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {doReleaseShared();return true;}return false;
}
復制代碼

當成功釋放同步狀態之后即tryReleaseShared會繼續執行doReleaseShared方法:

private void doReleaseShared() {/** Ensure that a release propagates, even if there are other* in-progress acquires/releases.  This proceeds in the usual* way of trying to unparkSuccessor of head if it needs* signal. But if it does not, status is set to PROPAGATE to* ensure that upon release, propagation continues.* Additionally, we must loop in case a new node is added* while we are doing this. Also, unlike other uses of* unparkSuccessor, we need to know if CAS to reset status* fails, if so rechecking.*/for (;;) {Node h = head;if (h != null && h != tail) {int ws = h.waitStatus;if (ws == Node.SIGNAL) {if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))continue;            // loop to recheck casesunparkSuccessor(h);}else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue;                // loop on failed CAS}if (h == head)                   // loop if head changedbreak;}
}
復制代碼

這段方法跟獨占式鎖釋放過程有點點不同,在共享式鎖的釋放過程中,對于能夠支持多個線程同時訪問的并發組件,必須保證多個線程能夠安全的釋放同步狀態,這里采用的CAS保證,當CAS操作失敗continue,在下一次循環中進行重試。

4.3 可中斷(acquireSharedInterruptibly()方法),超時等待(tryAcquireSharedNanos()方法)

關于可中斷鎖以及超時等待的特性其實現和獨占式鎖可中斷獲取鎖以及超時等待的實現幾乎一致,具體的就不再說了,如果理解了上面的內容對這部分的理解也是水到渠成的。

通過這篇,加深了對AQS的底層實現更加清楚了,也對了解并發組件的實現原理打下了基礎,學無止境,繼續加油:);如果覺得不錯,請給贊,嘿嘿。

參考文獻

《java并發編程的藝術》

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/278053.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/278053.shtml
英文地址,請注明出處:http://en.pswp.cn/news/278053.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

終于收到HacktoberFest的獎品啦

去年10月份給微軟repo提交了5個PR&#xff0c;達成了HacktoberFest 2018&#xff0c;今天終于收到了從美國到澳洲&#xff0c;飄洋過海&#xff0c;姍姍來遲的T-shirt&#xff0c;不過大小正好。算是新年禮物了&#xff0c;哈哈

三、SpringBoot-application.properties配置文件和application.yml配置文件

其實SpringBoot的配置文件有.properties和.yml兩種形式&#xff0c;兩種配置文件的效果類似&#xff0c;只不過是格式不同而已&#xff0c;孩兒們可以根據下面這幾種張截圖&#xff0c;通過對比端口號的配置&#xff0c;以及連接SQLServer數據庫的配置的書寫格式來自己體會兩者…

Teams中的快捷鍵讓溝通協作更加高效

使用Teams的快捷鍵可以幫助我們提高日常溝通協作的效率。 一、快捷鍵分類 1.常規2.導航3.聊天輸入界面4.團隊和會議 1.常規 功能桌面版本快捷鍵WebApp版本快捷鍵搜索CtrlECtrlE顯示命令CtrlSlash (/)CtrlSlash (/)gotoCtrlGCtrlShiftG開始新聊天CtrlNAltN打開設置CtrlComma …

線程間的協作(2)——生產者與消費者模式

2019獨角獸企業重金招聘Python工程師標準>>> 1.何為生產者與消費者 在線程世界里&#xff0c;生產者就是生產數據的線程&#xff0c;消費者就是消費數據的線程。 import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.…

一位面試者提到直接調用vuex中mutations方法

簡述是用this.$store.mutations.xxx(xx)方式調用&#xff0c;因從未見過此種調用方式&#xff0c;回來就搜索了一下&#xff0c;查詢結果如下 首先前文&#xff1a; 獲取 state 的方式有兩種&#xff0c;分別是 this.$store.state.num 這種直接獲取的方式&#xff0c;以及通過 …

從無到有到完善 - Teams抽獎機器人開發歷程

我沒有寫博客有2&#xff0c;3個月了&#xff0c;好幾個朋友來問我怎么不繼續了。實際上這幾個月我受到微軟好友的鼓舞和鼓勵&#xff0c;再加上今年2月1日有幸成為了微軟中國區第一位Teams的MVP&#xff0c;所以決定不再停留于技術demo&#xff0c;而是使用微軟最新的技術開發…

殘差網絡

作用&#xff1a;使得深層網絡可以獲得更好的性能&#xff0c;沒有它&#xff0c;即使加深網絡的層數無法直接獲得性能的提升。 我的理解&#xff1a;1、使得低層的特征表示可以越層傳遞。 2、在反向傳播時LOSS可以直接訓練低層特征。 3、淺層網絡的恒等映射&#xff0c;深層網…

js閉包

閉包(closure)是Javacript語言的一個難點&#xff0c;也是它的特色&#xff0c;很多高級應用都要依靠閉包實現。 一、變量的作用域 要理解閉包&#xff0c;首先必須理解Javascript特殊的變量作用域。 變量的作用域無非就是兩種&#xff1a;全局變量和局部變量。 Javascript語言…

Teams的MessageExtension最新功能:Initiate actions

官方文檔到目前為止對這個initiate action的說明比較簡潔&#xff0c;由于沒有一步步的截圖和說明&#xff0c;從頭到尾看一遍可能還在云里霧里。 我一步步摸索著走了一遍&#xff0c;發現這個initiate action的功能如此強大&#xff0c;不敢獨享&#xff0c;所以寫此博文&…

Java枚舉根據key獲取value

package com.utcip.crm.common.constants; import com.utcip.crm.common.base.process.ScheduleStatusEnum; /** * 合同變更存儲mongodb 狀態值 * author jingfangnan * */ public enum ConstractMongoStatus { NEW(3,"新增"), UPDATE(2,"修改"), D…

Selenium-基礎操作

一、測試代碼 Test public void test() { WebDriver driver new FirefoxDriver(); // 打開當前包中的index頁面 driver.get("file:///D:/%E8%B5%B5%E6%AC%A2/Selenium/Selenium/src/com/html/index.html"); WaitSeconds(1000); // 清除用戶輸入 driver.findElement(…

開發針對特殊租戶的Teams機器人

有些朋友問到&#xff0c;如果想要開發一個bot針對于Teams的某些租戶&#xff0c;如何做&#xff1f;實際上微軟的Teams的SDK早就提供了類似的功能。 如果你使用的是Javascript/Node.JS開發&#xff0c;使用session.message.sourceEvent.tenant.id 就可以知道當前消息來自于哪…

行業看點 | 英特爾成功開發超導量子計算芯片 推動產業加速發展

量子計算將會成為下一次技術革命的核心&#xff0c;你可能認為它還很遙遠&#xff0c;實際上量子計算會比預料的來得早。近期&#xff0c;英特爾在量子芯片方面取得突破&#xff0c;讓量子計算朝著現實前進了一大步。 繼IBM公司發布了自主量子處理器&#xff0c;谷歌著手研究基…

Teams App抽獎機器人 - 基礎架構

今天我們來聊一下&#xff0c;一個Teams app的infrastructure&#xff0c;我在考慮LuckyDraw的主要出于這么幾個出發點&#xff1a; 可管理性。因為這是一個個人產品&#xff0c;以后維護工作也只有我一個人&#xff0c;所以我希望整個infrastructure簡單、易管理&#xff0c;不…

Teams Bot的ServiceLevel測試

每一個Teams bot實際上就是一個web api服務&#xff0c;這個服務通過Bot Framework和Teams進行通訊&#xff0c;所以對于Teams app的測試就是對于一個api service的測試。 軟件行業發展到如今&#xff0c;測試技術已經趨于成熟。單元測試&#xff0c;冒煙測試&#xff0c;整合…

BZOJ1016:[JSOI2008]最小生成樹計數——題解

https://www.lydsy.com/JudgeOnline/problem.php?id1016 現在給出了一個簡單無向加權圖。你不滿足于求出這個圖的最小生成樹&#xff0c;而希望知道這個圖中有多少個不同的最小生成樹。&#xff08;如果兩顆最小生成樹中至少有一條邊不同&#xff0c;則這兩個最小生成樹就是不…

如何做Teams Bot的測試覆蓋

在我昨天的文章中介紹了如果對Teams bot做service level的測試&#xff0c;那到底要寫多少的測試代碼才算夠&#xff1f;如何才算測試到位了&#xff1f;這個時候我們就需要用”測試覆蓋率”來衡量&#xff0c;雖然覆蓋率高并不一定代表著就可以高枕無憂的以為我們軟件質量高了…

Spring Boot開發MongoDB應用實踐

本文繼續上一篇定時任務中提到的郵件服務&#xff0c;簡單講解Spring Boot中如何使用MongoDB進行應用開發。 上文中提到的這個簡易郵件系統大致設計思路如下&#xff1a; 1、發送郵件支持同步和異步發送兩種 2、郵件使用MongDB進行持久化保存 3、異步發送&#xff0c;直接將郵件…

Teams Bot如何做全球化

Office365在全球有大量的用戶&#xff0c;可以說是擁有最多用戶的商業SaaS平臺。Teams最近在發展迅猛&#xff0c;有1300萬日活用戶&#xff0c;已經超越了Slack。? Microsoft Teams overtakes Slack with 13 million daily users 我在設計Teams LuckyDraw bot的時候就希望我…

QuickBI助你成為分析師-郵件定時推送

創建報表過程中經常需要將報表情況定時推送給其他用戶&#xff0c;及時了解數據情況。高級版本郵件推送功能支持儀表板周期性推送到訂閱人&#xff0c;默認以當前登錄者視角查看&#xff0c;同時支持結合 行級權限進行權限控制 和 結合全局參數功能確定郵件推送內容參數&#x…