再學Android之多線程

多線程

對於如何新建線程,join,yield,setDaemon,線程狀態等這些簡單知識就不作多介紹了,下面分享下咱們平常開發中 經常使用的一些知識,多線程這塊的知識不少,後續會慢慢完善,若有錯誤和不足,還請各位大佬指出。java

線程池

好處:減少頻繁建立線程帶來的巨大的性能開銷,同時設定了上線,能夠防止建立線程數量過多致使系統崩潰的現象 先來看下java爲咱們提供的四種已經建立好的線程池:數組

  • newSingleThreadExecutor(使用阻塞隊列:LinkedBlockingQueue) 建立單個線程的線程池,也就是隻有一個線程,那麼,全部提交上來的任務就是按順序執行。若是這個線程發生異常的話,那麼 會有一個新的線程來替代它
  • newFixedThreadPool(使用阻塞隊列:LinkedBlockingQueue) 建立固定線程數量的線程池,每提交一個任務就會建立一個線程,直到達到線程池的最大大小。若是其中某個線程發生異常的話, 那麼會有一個新的線程來替代它
  • newCachedThreadPool(使用阻塞隊列:SynchronousQueue) 建立一個大小不受限制的,可緩存的,線程數量自動擴容,自動銷燬的線程池,若是線程池的大小超過了處理任務所須要的線程,那麼線程池會回收部分線程(時間是60s)
  • newScheduledThreadPool (使用阻塞隊列:DelayedWorkQueue) 建立一個大小無限的線程池,支持定時任務和週期性執行的任務

怎麼使用?緩存

調用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 任務是按照目標執行時間進行排序。

ArchTaskExecutor

閱讀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() {

            }
        });
複製代碼

Collections.synchronizedList

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登場

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

跟同步機制相似,都是爲了解決多線程訪問同一變量形成的訪問衝突問題,不一樣的的是: 同步機制是經過鎖機制犧牲時間來達到多線程安全訪問的目的; ThreadLocal爲每一個線程提供獨立的變量副本,犧牲空間保證每一個線程訪問相同變量時能夠獲得本身專屬的變量,達到多線程安全訪問的目的。

volatile

對於過可見性、有序性及原子性問題,一般狀況下咱們能夠經過Synchronized關鍵字來解決這些個問題,不過若是對Synchronized原理有了解的話,應該知道Synchronized是一個比較重量級的操做,對系統的性能有比較大的影響,因此,若是有其餘解決方案,咱們一般都避免使用Synchronized來解決問題。而volatile關鍵字就是Java中提供的另外一種解決可見性和有序性問題的方案。對於原子性,須要強調一點,也是你們容易誤解的一點:對volatile變量的單次讀/寫操做能夠保證原子性的,如long和double類型變量,可是並不能保證i++這種操做的原子性,由於本質上i++是讀、寫兩次操做

相關文章
相關標籤/搜索