前言:
有好長一段時間沒有寫博客了,思來想去,懶是主要的原因,另外一方面筆者在年初換了工作,適應工作還是花了蠻長一段時間的,還有就是,沒找到一個合適的主題、系列來寫。
之前寫的很多對個人的成長還是比較有益的,起碼現在在看源碼的時候不像前兩年那麼吃力了,寫的代碼也會刻意去模仿源碼的風格來做。
現在找到一個合適的主題,就是Sentinel,因爲公司未來要引入這項技術,所以筆者先前期研究下。
看代碼多了的好處就是,大家處理問題的方式大都大同小異,優秀的方式大家都在相互借鑑。筆者也想把這些閃光點單獨整理出來。所以暫時的這個系列不會像之前Spring那樣給與整體框架分析,而是反過來,從細微處見真章。這樣印象應該會更深刻些。當然在最後的時候也是要好好分析一下整體框架的。
注意:後面所使用的Sentinel版本爲1.7-Realease版本
1.JDK中關於線程池的使用
線程池是我們大家都比較熟悉的一個點了,當我們想併發執行任務時,首先想到的就是開啓多個線程來執行。
最簡單的方式莫過於直接創建一個線程任務,如下所示:
new Thread(new Runnable() {
@Override
public void run() {
// do your task here ...
}
}).start();
這樣直接創建線程的方式壞處也是顯而易見的,當任務過多時,每個任務都創建一個線程,那麼大量CPU時間都浪費在線程的切換,而且線程過多時也是會佔用大量內存的,所以在實際使用時我們一般也不會採用這種方式(少量任務,新建一兩個線程也是可以的)。
下面我們看下JDK提供的直接使用線程池的方案:
// 固定線程池 線程數量爲10
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.submit(new Runnable() {
@Override
public void run() {
// do your task here ...
}
});
我們比較常用上述方式來做,那麼問題來了,這種方式有什麼不好的嘛?
2.阿里代碼規範關於線程池的描述
在阿里的代碼規範中,有關於線程池的描述,如下
1. 線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。
1)FixedThreadPool 和 SingleThreadPool: 允許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool: 允許的創建線程數量爲 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM。
直接來分析下這段代碼:
// Executors.newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 關於LinkedBlockingQueue 的創建
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
// 可以看到容量基本是不限量,這樣當消費速度過慢時,數據會不斷的被寫入鏈表,內存有被耗盡的風險
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
3.Sentinel中關於線程池的使用
Sentinel中有大量的線程池使用的情況,下面來截取一段代碼(ZookeeperDataSource.java)
// ZookeeperDataSource.java中,在成員變量中定義了一個線程池
private final ExecutorService pool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(1), new NamedThreadFactory("sentinel-zookeeper-ds-update"),
new ThreadPoolExecutor.DiscardOldestPolicy());
// NamedThreadFactory定義
public class NamedThreadFactory implements ThreadFactory {
private final ThreadGroup group;
// 自增線程序列號
private final AtomicInteger threadNumber = new AtomicInteger(1);
// 可以自定義線程名稱
private final String namePrefix;
// 定義是否守護線程
private final boolean daemon;
public NamedThreadFactory(String namePrefix, boolean daemon) {
this.daemon = daemon;
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
this.namePrefix = namePrefix;
}
public NamedThreadFactory(String namePrefix) {
this(namePrefix, false);
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + "-thread-" + threadNumber.getAndIncrement(), 0);
t.setDaemon(daemon);
return t;
}
}
可以看到,Sentinel在創建線程池時
1.首先自定義一個ThreadFactory,(Executors會使用默認的DefaultThreadFactory,兩個大同小異)。
2.通過手動new 一個 ThreadPoolExecutor,自定義其中的參數來創建線程池,我們來分析下這個線程池的參數:
corePoolSize:1
maxPoolSize:1
workQueue:ArrayBlockQueue(1) // 長度爲1,通過控制這個長度,來放置內存溢出
rejectPolicy:discardOldestPolicy // 拋棄最先的一個任務
總結:
以後我們在創建線程池的時候,儘量避免直接使用Executors來創建線程池。
通過自定義一個ThreadPoolExecutor來創建線程池,自定義其中的參數即可。
參考:https://github.com/alibaba/Sentinel