如果一個專案總用單執行緒來跑,難免會遇到一些效能問題,所以再開發中,我們應該儘量適量的使用多執行緒(在保證執行緒安全的情況下)。
本教程大概目錄:
- 模擬單執行緒情節
- 用Callable實現 併發程式設計
- 用DeferedResult實現非同步處理
模擬單執行緒情節
/**
* Created by Fant.J.
*/
@RestController
@Slf4j
public class AsyncController {
/**
* 單執行緒測試
* @return
* @throws InterruptedException
*/
@RequestMapping("/order")
public String order() throws InterruptedException {
log.info("主執行緒開始");
Thread.sleep(1000);
log.info("主執行緒返回");
return "success";
}
}
複製程式碼
我們把執行緒休息一秒當作模擬處理業務所花費的時間。很明顯能看出來,這是個單執行緒。
nio-8080-exec-1
表示主執行緒的執行緒1。
用Callable實現 併發程式設計
/**
* 用Callable實現非同步
* @return
* @throws InterruptedException
*/
@RequestMapping("/orderAsync")
public Callable orderAsync() throws InterruptedException {
log.info("主執行緒開始");
Callable result = new Callable() {
@Override
public Object call() throws Exception {
log.info("副執行緒開始");
Thread.sleep(1000);
log.info("副執行緒返回");
return "success";
}
};
log.info("主執行緒返回");
return result;
}
複製程式碼
我們可以看到,主執行緒的開始和返回(結束處理)是首先執行的,然後副執行緒才執行真正的業務處理。說明主執行緒在這裡的作用是呼叫(喚醒)子執行緒,子執行緒處理完會返回一個Object物件,然後返回給使用者。
這樣雖然實現了併發處理,但是有一個問題,就是主執行緒和副執行緒沒有做到完全分離,畢竟是一個巢狀進去的副執行緒。
所以為了優化我們的實現,我在這裡模擬 訊息中介軟體 來實現主執行緒副執行緒的完全分離。
用DeferedResult實現非同步處理
因為本章主要講的是併發程式設計原理,所以這裡我們不用現成的訊息佇列來搞,我們模擬一個訊息佇列來處理。
MockQueue .java
/**
* 模擬訊息佇列 類
* Created by Fant.J.
*/
@Component
@Slf4j
public class MockQueue {
//下單訊息
private String placeOrder;
//訂單完成訊息
private String completeOrder;
public String getPlaceOrder() {
return placeOrder;
}
public void setPlaceOrder(String placeOrder) throws InterruptedException {
new Thread(()->{
log.info("接到下單請求"+placeOrder);
//模擬處理
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//給completeOrder賦值
this.completeOrder = placeOrder;
log.info("下單請求處理完畢"+placeOrder);
}).start();
}
public String getCompleteOrder() {
return completeOrder;
}
public void setCompleteOrder(String completeOrder) {
this.completeOrder = completeOrder;
}
}
複製程式碼
注意再setPlaceOrder(String placeOrder)方法裡,我建立了一個新的執行緒來處理接單的操作(為什麼要建立新執行緒,怕主執行緒在這掛起,此段邏輯也沒有執行緒安全問題,況且非同步處理更快)。傳進來的引數是個 訂單號 ,經過1s的處理成功後,把訂單號傳給completeOrder 欄位,說明使用者下單成功,我在下面付controller呼叫該方法的程式碼
//注入模擬訊息佇列類
@Autowired
private MockQueue mockQueue;
@Autowired
private DeferredResultHolder deferredResultHolder;
....
@RequestMapping("/orderMockQueue")
public DeferredResult orderQueue() throws InterruptedException {
log.info("主執行緒開始");
//隨機生成8位數
String orderNumber = RandomStringUtils.randomNumeric(8);
mockQueue.setPlaceOrder(orderNumber);
DeferredResult result = new DeferredResult();
deferredResultHolder.getMap().put(orderNumber,result);
Thread.sleep(1000);
log.info("主執行緒返回");
return result;
}
複製程式碼
好了,然後我們還需要一箇中介類來存放訂單號和處理結果。為什麼需要這麼一個類呢,因為我們之前說過要實現主執行緒和副執行緒分離,所以需要一箇中介來存放處理資訊(比如:這個訂單號資訊,和處理結果資訊),我們判斷處理結果是否為空就知道該副執行緒執行了沒有。所以我們寫一箇中介類DeferredResultHolder 。
######DeferredResultHolder .java
/**
* 訂單處理情況 中介/持有者
* Created by Fant.J.
*/
@Component
public class DeferredResultHolder {
/**
* String: 訂單號
* DeferredResult:處理結果
*/
private Map<String,DeferredResult> map = new HashMap<>();
public Map<String, DeferredResult> getMap() {
return map;
}
public void setMap(Map<String, DeferredResult> map) {
this.map = map;
}
}
複製程式碼
在重複一次-.-,為什麼需要這麼一個類呢,因為我們之前說過要實現主執行緒和副執行緒分離,所以需要一箇中介來存放處理資訊(比如:這個訂單號資訊,和處理結果資訊),一個訂單肯定要對應一個結果。不然豈不是亂了套。
DeferredResult是用來放處理結果的物件。
好了,那新問題又來了,我們怎麼去判斷訂單處理成功了沒有,我們此時就需要寫一個監聽器,過100毫秒監聽一次MockQueue類中的completeOrder中是否有值,如果有值,那麼這個訂單就需要被處理。我們寫一個監聽器。
QueueListener .java
/**
* Queue監聽器
* Created by Fant.J.
*/
@Component
@Slf4j
public class QueueListener implements ApplicationListener<ContextRefreshedEvent>{
@Autowired
private MockQueue mockQueue;
@Autowired
private DeferredResultHolder deferredResultHolder;
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
new Thread(()->{
while(true){
//判斷CompleteOrder欄位是否是空
if (StringUtils.isNotBlank(mockQueue.getCompleteOrder())){
String orderNumber = mockQueue.getCompleteOrder();
deferredResultHolder.getMap().get(orderNumber).setResult("place order success");
log.info("返回訂單處理結果");
//將CompleteOrder設為空,表示處理成功
mockQueue.setCompleteOrder(null);
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
複製程式碼
我們可以看到一共有三個不同的執行緒來處理。
分割線後,我再給大家帶來一批乾貨,自定義執行緒池 https://www.jianshu.com/p/832f2b162450
學完這個後,再看下面的。。
我們前面的程式碼中,有兩部分有用new Thread()來建立執行緒,我們有自己的執行緒池後,就可以用執行緒池來分配執行緒任務了,我在自定義執行緒裡有講,我用的是第二種配置方法(用@Async註解來給執行緒 )。 修改如下:
@Async
public void setPlaceOrder(String placeOrder) throws InterruptedException {
log.info("接到下單請求"+placeOrder);
//模擬處理
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//給completeOrder賦值
this.completeOrder = placeOrder;
log.info("下單請求處理完畢"+placeOrder);
}
複製程式碼
我們看看效果:
圈紅圈的就是我們自己定義的執行緒池裡分配的執行緒。
謝謝大家!