Android執行緒管理之ThreadLocal理解及應用場景

總李寫程式碼發表於2016-08-29

前言:

     最近在學習總結Android的動畫效果,當學到Android屬性動畫的時候大致看了下原始碼,裡面的AnimationHandler存取使用了ThreadLocal,激起了我很大的好奇心以及興趣!查閱了一下資料發現Android最重要的Handler訊息機制裡面的Looper儲存也是採用ThreadLocal,開源框架EventBus儲存當前執行緒下的傳送事件佇列狀態也是採用ThreadLocal,那麼為何要使用ThreadLocal呢?ThreadLocal是什麼呢?它能解決什麼樣的問題呢?帶著這麼疑問來學習下ThreadLocal。

     執行緒管理相關文章地址:

ThreadLocal介紹

   ThreadLocal如果單純從字面上理解的話好像是“本地執行緒”的意思,其實並不是這個意思,只是這個名字起的太容易讓人誤解了,它的真正的意思是執行緒本地變數。看看官方怎麼說的。

/**
 * Implements a thread-local storage, that is, a variable for which each thread
 * has its own value. All threads share the same {@code ThreadLocal} object,
 * but each sees a different value when accessing it, and changes made by one
 * thread do not affect the other threads. The implementation supports
 * {@code null} values.
 *
 * @see java.lang.Thread
 * @author Bob Lee
 */

   哈哈作為學渣英語不好,藉助百度翻譯了一下。翻譯如下也不知道對不對

   實現一個執行緒本地的儲存,也就是說,每個執行緒都有自己的區域性變數。所有執行緒都共享一個ThreadLocal物件,但是每個執行緒在訪問這些變數的時候能得到不同的值,每個執行緒可以更改這些變數並且不會影響其他的執行緒,並且支援null值。

ThreadLocal理解

我們先看下屬性動畫為每個執行緒設定AnimationHeadler的

    private static AnimationHandler getOrCreateAnimationHandler() {
        AnimationHandler handler = sAnimationHandler.get();
        if (handler == null) {
            handler = new AnimationHandler();
            sAnimationHandler.set(handler);
        }
        return handler;
    }

因為protected static ThreadLocal<AnimationHandler> sAnimationHandler =new ThreadLocal<AnimationHandler>();這裡沒有采用初始化值,這裡不是通過一個變數的拷貝而是每個執行緒通過new建立一個物件出來然後儲存。很多人認為ThreadLocal是為了解決共享物件的多執行緒訪問問題的,這是錯誤的說法,因為無論是通過初始化變數的拷貝還是直接通過new建立自己區域性變數,ThreadLocal.set() 到執行緒中的物件是該執行緒自己使用的物件,其他執行緒是不需要訪問的,也訪問不到的。各個執行緒中訪問的是不同的物件,改變的也是自己獨立的物件,本身就不屬於同一個物件,沒有共享的概念,更加不可能是解決共享物件的多執行緒訪問的。

通過上面的理解總結以下幾點

 1.每個執行緒讀擁有自己的區域性變數

     每個執行緒都有一個獨立於其他執行緒的上下文來儲存這個變數,一個執行緒的本地變數對其他執行緒是不可見的

 2.獨立於變數的初始化副本,或者初始化一個屬於自己的變數

     ThreadLocal可以給一個初始值,而每個執行緒都會獲得這個初始化值的一個副本,這樣才能保證不同的執行緒都有一份拷貝,同樣也可以new的方式為執行緒建立一個變數

 3.變數改變只與當前執行緒關聯,執行緒之間互不干擾

    ThreadLocal 不是用於解決共享變數的問題的,不是為了協調執行緒同步而存在,而是為了方便每個執行緒處理自己的狀態而引入的一個機制。

所以ThreadLocal既不是為了解決共享多執行緒的訪問問題,更不是為了解決執行緒同步問題,ThreadLocal的設計初衷就是為了提供執行緒內部的區域性變數,方便在本執行緒內隨時隨地的讀取,並且與其他執行緒隔離。

ThreadLocal使用場景

    說了那麼多的概念,歸根到底我們在什麼時候才使用ThreadLocal呢?很多時候我們會建立一些靜態域來儲存全域性物件,那麼這個物件就可能被任意執行緒訪問,如果能保證是執行緒安全的,那倒是沒啥問題,但是有時候很難保證執行緒安全,這時候我們就需要為每個執行緒都建立一個物件的副本,我們也可以用ConcurrentMap<Thread, Object>來儲存這些物件,這樣會比較麻煩,比如當一個執行緒結束的時候我們如何刪除這個執行緒的物件副本呢?如果使用ThreadLocal就不用有這個擔心了,ThreadLocal保證每個執行緒都保持對其執行緒區域性變數副本的隱式引用,只要執行緒是活動的並且 ThreadLocal 例項是可訪問的;線上程消失之後,其執行緒區域性例項的所有副本都會被垃圾回收(除非存在對這些副本的其他引用)。經查閱資料大致得到以下兩種場景:

1.)當某些資料以執行緒為作用域,並且不同執行緒擁有不同資料副本的時候。

   ThreadLocal使用場合主要解決多執行緒中資料因併發產生不一致的問題。ThreadLocal以空間換時間,為每個執行緒的中併發訪問的資料提供一個副本,通過訪問副本來執行業務,這樣的結果是耗費了記憶體,但大大減少了執行緒同步所帶來的執行緒消耗,也減少了執行緒併發控制的複雜度。

   例如Android的Handler訊息機制,對於Handler來說,它需要獲取當前執行緒的looper很顯然Looper的作用域就是執行緒並且不同執行緒具有不同的Looper,這個時候通過ThreadLocal就可以輕鬆實現Looper線上程中的存取。再例如開源框架EventBus,EventBus需要獲取當前執行緒的PostingThreadState物件,不同的PostingThreadState同樣作用於不同的執行緒,EventBus可以很輕鬆的獲取當前執行緒下的PostingThreadState物件,然後進行相關操作。

2.)複雜邏輯下物件傳遞,比如監聽器的傳遞

   使用引數傳遞的話:當函式呼叫棧更深時,設計會很糟糕,為每一個執行緒定義一個靜態變數監聽器,如果是多執行緒的話,一個執行緒就需要定義一個靜態變數,無法擴充套件,這時候使用ThreadLocal就可以解決問題。

 ThreadLocal使用舉例

  舉一個簡單的例子,讓每個執行緒擁有自己唯一的一個任務佇列,類似EventBus的實現。

  private static final ThreadLocal<PriorityQueue<TaskItem>> queueThreadLocal = new ThreadLocal<PriorityQueue<TaskItem>>() {
        @Override
        protected PriorityQueue<TaskItem> initialValue() {
            return new PriorityQueue<>(5);
        }
    };


    public PriorityQueue<TaskItem> getTaskQueue() {
        PriorityQueue<TaskItem> taskItems = queueThreadLocal.get();
        return taskItems;
    }


    public void addTask(TaskItem taskItem) {
        PriorityQueue<TaskItem> taskItems = queueThreadLocal.get();
        taskItems.add(taskItem);
    }

    public void removeTask(TaskItem taskItem) {
        PriorityQueue<TaskItem> taskItems = queueThreadLocal.get();
        if (taskItems.contains(taskItem)) {
            taskItems.remove(taskItem);
        }
    }

    private void exceTask() {
        PriorityQueue<TaskItem> taskItems = queueThreadLocal.get();
        if (!taskItems.isEmpty()) {
            TaskItem taskItem = taskItems.poll();
            taskItem.exceTask();
        }
    }

 附上TaskItme程式碼:

public class TaskItem implements Comparable {
    private long Id;
    private String name;
    private int priority;

    public long getId() {
        return Id;
    }

    public void setId(long id) {
        Id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPriority() {
        return priority;
    }

    public void setPriority(int priority) {
        this.priority = priority;
    }

    @Override
    public int compareTo(Object arg0) {
        if (TaskItem.class.isInstance(arg0)) {
            TaskItem tm = (TaskItem) arg0;
            if (tm.priority > priority) {
                return -1;
            } else if (tm.priority < priority) {
                return 1;
            }
        }
        return 0;
    }

    public void exceTask() {
        Log.e("exceTask", "exceTask---id:" + Id + " name:" + name);
    }

}

經過上面程式碼可以看到,你是在哪個執行緒提交的任務自然而然的就新增到執行緒所屬的任務佇列裡面,這裡其實通過ConcurrentMap<Thread, Object>儲存也是可以的,上面也說了相對比較麻煩。

 

總結:

   由於對ThreadLocal瞭解不是很深刻,僅僅理解那麼一點點,也許有些觀點不一定正確,希望看到的朋友批評指正謝謝。如果大家想通過一個例子來學習的話,個人建議看下EventBus中使用ThreadLocal範例,用的很巧妙又很容易讓人理解,只是ThreadLocal本身我們在日常專案開發中使用的比較少,一會半會的很難找到合適的場景來搞懂它。

 

相關文章