對於如何新建線程,join,yield,setDaemon,線程狀態等這些簡單知識就不作多介紹了,下面分享下咱們平常開發中 經常使用的一些知識,多線程這塊的知識不少,後續會慢慢完善,若有錯誤和不足,還請各位大佬指出。java
好處:減少頻繁建立線程帶來的巨大的性能開銷,同時設定了上線,能夠防止建立線程數量過多致使系統崩潰的現象 先來看下java爲咱們提供的四種已經建立好的線程池:數組
怎麼使用?緩存
調用execute(runnable)或者submit(runnable)便可,兩者有什麼區別? sumit能夠返回一個Future來獲取執行的結果安全
上面四種線程池的構造函數以下:bash
/**
* @param corePoolSize 核心線程數量, 超出數量的線程會進入阻塞隊列
* @param maximumPoolSize 最大可建立線程數量
* @param keepAliveTime 線程存活時間
* @param unit 存活時間的單位
* @param workQueue 線程溢出後的阻塞隊列
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new
LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new Executors.FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new
LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new
SynchronousQueue<Runnable>());
}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS, new
ScheduledThreadPoolExecutor.DelayedWorkQueue());
}
複製代碼
上面的4種線程池已經知足咱們平常開發中絕大部分的場景,可是有他們本身的弊端:多線程
FixedThreadPool和SingleThreadPoolPool : 請求隊列使用的是LinkedBlockingQueue,容許的請求隊列的長度是Integer.MAX_VALUE,可能會堆積大量的請求,從而致使 OOM.ide
CachedThreadPool和ScheduledThreadPool : 容許建立的線程數量是Integer.MAX_VALUE,可能會建立大量的線程池, 從而致使 OOM.函數
以上也就是阿里建議咱們本身配置線程池的緣由,那麼線程池的那些參數究竟該如何配置? 線程池大小的配置: 通常根據任務類型 和 CPU的核數(N)工具
CPU密集型的任務多的話,須要減小線程的數量,這樣能夠下降切換線程帶來的開銷,建議配置線程池的大小爲: N+1 IO密集型的任務到的話,須要增長線程的數量,建議配置爲 N*2post
咱們來看一下AsyncTask的線程池配置:
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE_SECONDS = 30;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
public static final Executor THREAD_POOL_EXECUTOR;
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
sPoolWorkQueue, sThreadFactory);
threadPoolExecutor.allowCoreThreadTimeOut(true);
THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
複製代碼
從AsyncTask的源碼中咱們得知,核心線程的數量是至少有2線程,最多有4個線程,最好狀況是有CPU的核心數-1,這樣能夠避免CPU在 後臺工做的時候達到飽和。最大線程數量設置的是CPU_COUNT * 2 + 1,使用的阻塞隊列是LinkedBlockingQueue,默認大小是 Integer.MAX_VALUE,可是AsyncTask使用時指定了其大小爲128,下面說下阻塞隊列:
一、ArrayBlockingQueue
是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
二、LinkedBlockingQueue
一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量一般要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()和newSingleThreadExecutor使用了這個隊列
三、SynchronousQueue
一個不存儲元素的阻塞隊列。每一個插入操做必須等到另外一個線程調用移除操做,不然插入操做一直處於阻塞狀態,吞吐量一般要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
四、PriorityBlockingQueue
一個具備優先級的無限阻塞隊列。 五、DelayedWorkQueue 任務是按照目標執行時間進行排序。
閱讀LiveData源碼的時候發現有這個API,看名字就是到該怎麼使用,這裏就不做多介紹了,下面看代碼
ArchTaskExecutor.getIOThreadExecutor().execute(new Runnable() {
@Override
public void run() {
}
});
ArchTaskExecutor.getMainThreadExecutor().execute(new Runnable() {
@Override
public void run() {
}
});
ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() {
@Override
public void run() {
}
});
ArchTaskExecutor.getInstance().postToMainThread(new Runnable() {
@Override
public void run() {
}
});
複製代碼
ArrayList是非線程安全的,在多線程中,對ArrayList進行迭代的時候,會報ConcurrentModificationException錯誤, 爲了保證同步,咱們可使用集合工具類來對ArrayList進行包裝,即Collections.synchronizedList。同時咱們也知道 Vector也是線程安全的,那麼這二者有什麼區別呢? Vector底層是數組實現的,使用同步方法來實現線程安全,Collections.synchronizedList並未修改list的底層實現,只是 對其進行了一層包裝,使用同步代碼塊來實現線程安全。同步方法的鎖是做用在方法上,因此鎖的粒度會比同步代碼塊的大,同步代碼的 執行效率是與鎖的粒度成反比的,因此Collections.synchronizedList的執行效率是高於Vector,並且,Vector的全部對集合 的操做都加上了synchronized關鍵字,效率較低,已不推薦使用。 可是Collections.synchronizedList真的就線程安全了嗎?
List<Integer> integers = Collections.synchronizedList(new ArrayList<Integer>());
Iterator<Integer> iterator = integers.iterator();
while (iterator.hasNext()){
doSth(iterator.next());
}
複製代碼
屢次運行後咱們會發現依然會報ConcurrentModificationException錯誤,不是已是線程安全了嗎,爲何呢? 那有同窗可能會說是否是迭代器的問題,我換成for-Each來遍歷,for-Each只是簡潔,數組索引的邊界值只計算一次罷了, 試一下,一樣會報ConcurrentModificationException的錯誤。那麼究竟爲何? Collections.synchronizedList雖然每一個操做已經使用靜態代碼塊,可是在迭代的時候,整個的迭代過程不是原子性的, 想要結果正確,咱們應該對整個迭代過程進行加鎖處理:
List<Integer> integers = Collections.synchronizedList(new ArrayList<Integer>());
synchronized (integers){
Iterator<Integer> iterator = integers.iterator();
while (iterator.hasNext()){
doSth(iterator.next());
}
}
複製代碼
看到上面的代碼,咱們不由感到恐怖如斯,遍歷一遍數據咱們都要加鎖,那不是很慢嘛,因而,CopyOnWriteArrayList登場
咱們看下JUC(java.util.concurrent)包下面的線程安全的類是怎樣替代老一代的線程安全的類的: Hashtable -> ConcurrentHashMap Vector -> CopyOnWriteArrayList 其實他們之間最大的不一樣就是加鎖粒度的不一樣,新一代的容器的加鎖粒度更小,好比ConcurrentHashMap用了cas鎖、volatile等方式來實現線程安全。 注:JUC下面的安全容器在遍歷的時候不會拋出ConcurrentModificationException的異常的。 那麼下面咱們來學習下CopyOnWriteArrayList: 先來看下COW: 若是有多個調用者同時請求相同的資源的時候,他們會獲取到相同的指針而且指向相同的資源,知道某一個調用者試圖修改這個資源的時候,系統纔會真正複製一份副本給到這個調用者,而其餘的調用者看到的依然是以前的資源。 經過上面的概念咱們就能夠得出一個結論了:在讀操做比較多的狀況下,使用COW效率更高。
CopyOnWriteArrayList#add()在添加的時候就上鎖,並複製一個新數組,增長操做在新數組上完成,將array指向到新數組中,最後解鎖 寫加鎖,讀不加鎖 問題:爲何在遍歷的時候,不用調用者顯式的加鎖呢? CopyOnWriteArrayList在使用迭代器遍歷的時候,操做的都是原數組! 缺點: 1.若是CopyOnWriteArrayList常常要增刪改裏面的數據,常常要執行add()、set()、remove()的話,那是比較耗費內存的 2.CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性
跟同步機制相似,都是爲了解決多線程訪問同一變量形成的訪問衝突問題,不一樣的的是: 同步機制是經過鎖機制犧牲時間來達到多線程安全訪問的目的; ThreadLocal爲每一個線程提供獨立的變量副本,犧牲空間保證每一個線程訪問相同變量時能夠獲得本身專屬的變量,達到多線程安全訪問的目的。
對於過可見性、有序性及原子性問題,一般狀況下咱們能夠經過Synchronized關鍵字來解決這些個問題,不過若是對Synchronized原理有了解的話,應該知道Synchronized是一個比較重量級的操做,對系統的性能有比較大的影響,因此,若是有其餘解決方案,咱們一般都避免使用Synchronized來解決問題。而volatile關鍵字就是Java中提供的另外一種解決可見性和有序性問題的方案。對於原子性,須要強調一點,也是你們容易誤解的一點:對volatile變量的單次讀/寫操做能夠保證原子性的,如long和double類型變量,可是並不能保證i++這種操做的原子性,由於本質上i++是讀、寫兩次操做