最近在Review代碼中看到有些同學在使用多線程的時候比較随意,基本上各種使用方式都有。但是有些寫法其實是适合standalone的程式,有的寫法在Spring中得到了簡化。是以建議大家在在SQI相關開發中可以嘗試用@Async注解的方式完成異步操作,既清晰明了,也簡單友善。
異步的幾種寫法
-
刀耕火種年代
這個年代是沒有線程池管理的,實作了Runnable接口後就start來運作。
-
大航海時代
這個年代已經開始使用線程池了,我們可以看一段示例代碼
這段代碼在standalone程式中看起來問題不大。但是在Web服務中每個Request都會來調用一次,會使得堆積的請求處理隊列消耗非常大的記憶體,極端情況下會引發OOM問題。ExecutorService executor = Executors.newFixedThreadPool(N); for (int i = 0; i < kProducts.size(); i++) { Products product = kProducts.get(i); if (pid != null || bizdate != null) { if (bizdate == null) { bizdate = "2015-04-01"; } executor.execute(new offlineKeludeJob(product, DateUtils.getMorning(DateUtils.parseDate(bizdate)))); } else { executor.execute(new offlineKeludeJob(product)); } } executor.shutdown();
- 工業化時代
在這裡大家使用Spring中的ThreadPoolTaskExecutor,使用起來也沒有特别大的問題。例如下面這個簡單的例子
<task:executor id="labelTaskExecutor" pool-size="100" queue-capacity="10000" rejection-policy="ABORT"/>
@Autowired
ThreadPoolTaskExecutor labelDbExecutor;
labelTaskExecutor.submit(new Runnable() {
@Override
public void run() {
try {
Integer ret = insert(tmpLabelData);
if (ret <= 0) {
logger.error(String.format("[ERRORLabelData]insert data failed! label data:%s", JSONObject.toJSONString(tmpLabelData)));
}
} catch (Exception e) {
logger.error(String.format("[ERRORLabelData]insert data into db exception!"), e);
}
}
}
);
@Async注解使用
上面說了那麼多使用者,其實都沒有什麼用。隻是用來突出關于Async注解的使用。最初的兩種方法都因為自身的不足,不建議在SQI項目中直接使用。而最後一個方法雖然沒有什麼問題,但是寫起來比較麻煩,還需要自己實作Runnable接口。那麼對于我們來說,有沒有更簡單友善的方式來使用呢?答案就是Spring 3.0帶來Async注解,其使用主要就隻有兩步。
- 在applicationContext.xml中增加線程池配置
<context:component-scan base-package="com.alibaba.search" /> <task:annotation-driven executor="defaultExecutor"/> <task:executor id="defaultExecutor" pool-size="100" queue-capacity="10000" rejection-policy="ABORT"/>
- 在你需要調用的方法上加上相應的注解
@Service public class TestClass { @Async public void asyncTest() { //Do something } }
@Async使用注意事項
- 類内部的方法間的調用是不起效果的,具體是在于Async是基于bean自動掃描後生成的proxy實作的。那麼一定要用怎麼辦,最簡單的辦法就是拆出到另外一個class裡面。
- 如果在多個XML中進行了配置是會覆寫的。例如SQI在web.xml中定義了
<servlet> <servlet-name>spring</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springMvcConfig.xml</param-value> </init-param> <load-on-startup>2</load-on-startup> </servlet>
那麼如果同時在springMvcConfig.xml和applicationContext.xml中有對task:executor的定義的話,DispatcherServlet中加載的XML會覆寫掉ContextLoaderListener中加載的。用我們的例子來講就是springMvcConfig.xml中的配置會覆寫掉applicationContext.xml。
- 如果要使用多個線程池怎麼辦?在使用的時候指定一下對應的ID就可以了。
<context:component-scan base-package="com.alibaba.search" /> <task:annotation-driven executor="defaultExecutor"/> <task:executor id="defaultExecutor" pool-size="100" queue-capacity="10000" rejection-policy="ABORT"/> <task:executor id="statisticExecutor" pool-size="100" queue-capacity="10000" rejection-policy="ABORT"/>
@Service
public class TestClass {
@Async
public void asyncTest() {
//Do something
}
@Async("statisticExecutor")
public void statisticAsyncTest() {
//Do some statistic
}
}