1. 程式人生 > >Java併發程式設計系列之二十七:ThreadLocal

Java併發程式設計系列之二十七:ThreadLocal

ThreadLocal簡介

ThreadLocal翻譯過來就是執行緒本地變數,初學者可能以為ThreadLocal是指一個Thread,其實說白了,ThreadLocal就是一個成員變數,只不過這是一個特殊的變數——變數值總是與當前執行緒(呼叫Thread.currentThread()得到)相關聯。既然ThreadLocal是一個變數,那麼其作用是是什麼呢?說得抽象點就是提供了執行緒封閉性,說得具體點就是為每個使用該變數的執行緒提供一個變數的副本,這樣每個使用該變數的執行緒都有一個副本,從而將執行緒之間對變數的訪問隔離開來了,對變數的操作互不影響。

當訪問共享的可變資料時(因為還有final型別的不可變資料),通常會使用同步機制,因為同步需要加鎖,所以在效率上可能會收到影響。一種避免使用同步的方式就是不共享資料。因為在單執行緒內訪問資料就不需要考慮同步。這就是對執行緒封閉的解釋,同時也是ThreadLocal設計的核心思想。當某個物件被執行緒封閉在一個執行緒內部時,該物件就自動實現了執行緒安全性。ThreadLocal具體做了什麼事呢?它使執行緒中的某個值與當前執行緒關聯在一起,實現“一處設定處處呼叫”。

所以對比同步機制與ThreadLocal,可以得出同步通過加鎖的方式實現了執行緒資料共享,也就是以時間換空間,而ThreadLocal則是以變數副本的方式通過以空間換時間的手段實現執行緒資料共享。

設計一個ThreadLocal

根據上面的描述,設計ThreadLocal的關鍵在於將值與訪問該值的物件,也就是當前執行緒,關聯起來。下面的程式碼實現了這一功能:

package com.rhwayfun.patchwork.concurrency.r0408;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by rhwayfun on 16-4-8.
 */
public class DemoThreadLocal { /** * 用來關聯值與當前執行緒的Map */ private Map<Thread,Object> localMap = Collections.synchronizedMap(new HashMap<Thread, Object>()); /** * 設定值與執行緒關聯 * @param copyValue */ public void set(Object copyValue){ //1、key為當前訪問值的執行緒,value為值的副本
localMap.put(Thread.currentThread(),copyValue); } /** * 得到當前執行緒關聯的值 * @return */ public Object get(){ //獲取當前執行緒 Thread currentThread = Thread.currentThread(); //根據當前執行緒得到值 Object value = localMap.get(currentThread); if (value == null || !localMap.containsKey(currentThread)){ value = initialValue(); localMap.put(currentThread,value); } return value; } /** * 對值進行初始化 * @return */ protected Object initialValue() { return null; } }

這大概就是一個最簡單版本的ThreadLocal了,在使用的時候把DemoThreadLocal作為內部私有的不可變類,就可以實現“一處設定處處呼叫”的簡單功能了。但是在工程實踐中,設計需要考慮的問題多得多,設計也就更復雜。

ThreadLocal的設計原理

ThreadLocal通常用於防止對可變的單例項變數或者全域性變數進行共享。在單執行緒中往往可能使用一個全域性的資料庫連線,這樣就可以避免在每次呼叫每個方法時都需要例項化該資料庫連線。通常在JDBC中使用的資料庫連線就使用到了ThreadLocal,每個執行緒都有一個屬於自己的資料庫連線,達到了執行緒隔離的目的。程式碼通常是這樣的:

package com.rhwayfun.patchwork.concurrency.r0408;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * Created by rhwayfun on 16-4-8.
 */
public class ConnectionManager {

    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
        @Override
        protected Connection initialValue() {
            Connection conn = null;
            try {
                conn = DriverManager.getConnection(
                        "jdbc:mysql://localhost:3306/test", "username",
                        "password");
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return conn;
        }
    };

    public static Connection getConnection() {
        return connectionHolder.get();
    }

    public static void setConnection(Connection conn) {
        connectionHolder.set(conn);
    }
}

上面的程式碼也演示瞭如何使用ThreadLocal,下面就分析一下ThreadLocal是如何實現將當前執行緒與訪問的值關聯起來的?其實原理和簡化版的實現是一樣的,都是通過一個map,不過在ThreadLocal的實現中,是ThreadLocalMap,它是ThreadLocal的一個變數,看程式碼就知道了:

    public void set(T value) {
        //得到當前執行緒
        Thread t = Thread.currentThread();
        //根據當前執行緒得到一個map
        ThreadLocalMap map = getMap(t);
        //如果map不為空則呼叫set進行關聯
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

上面的程式碼與簡化版實現如出一轍,首先根據當前執行緒得到ThreadLocalMap物件,如果map不為空則直接將當前執行緒與value(訪問的值)關聯起來;如果map為空則建立一個ThreadLocalMap。

通過原始碼可以發現ThreadLocalMap是ThreadLocal類的一個靜態內部類,它實現了鍵值對的設定和獲取(對比Map物件來理解),每個執行緒中都有一個獨立的ThreadLocalMap副本,它所儲存的值只能被當前執行緒讀取和修改。ThreadLocal類通過操作每一個執行緒特有的ThreadLocalMap副本,從而實現了變數訪問在不同執行緒中的隔離。因為每個執行緒的變數都是自己特有的,完全不會有併發錯誤。還有一點就是,ThreadLocalMap儲存的鍵值對中的鍵是this物件指的是ThreadLocal物件,而值就是你所設定的物件了(這裡是Connection)。

    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

從程式碼可以看到,getMap就是獲取一個名為threadLocals的變數,而這個變數的型別就是ThreadLocalMap,這就是說對於每個不同的執行緒都有一個ThreadLocalMap。這樣每個執行緒都有一個ThreadLocalMap,就可以實現執行緒之間的的隔離了。所以執行緒對變數的操作實際上都在各自的ThreadLocalMap儲存一份該值的副本。下面我們看看在ThreadLocalMap是如何設定的:

private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

如果熟悉HashMap,這實際上就是HashMap的一個put操作:首先在Entry陣列中判讀是否存在key為傳入的key的Entry,如果存在則覆蓋;如果key為null則進行替換。如果上述條件都不滿足則建立一個Entry物件放入Entry陣列中。

接下來,看看get方法是如何實現的:

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

在獲取和當前執行緒繫結的值時,ThreadLocalMap物件是以this指向的ThreadLocal物件為鍵進行查詢的,這和前面set()方法的程式碼是相呼應的。如果之前通過this作為key找到了則直接返回,如果沒有找到則呼叫setInitialValue()方法。該方法首先得到在實現程式碼初始化的value(在我們的程式碼中Connection,也就是說value是Connection),然後執行和之前set方法一樣的操作。

由於ThreadLocal使用的時候每個執行緒都有自己的ThreadLocalMap,那麼是否會出現OOM的問題呢?答案可以在以下的原始碼中得到答案:

static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

可以看到Entry物件是一個弱引用,根據弱引用的特點:在垃圾回收器執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。所以線上程終止後,ThreadLocalMap物件就會被當做垃圾回收掉,自然也就不用擔心記憶體洩露的問題了。

一個完整的ThreadLocal例子

package com.rhwayfun.patchwork.concurrency.r0408;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * Created by rhwayfun on 16-4-8.
 */
public class PersonThreadLocalDemo {

    private static final ThreadLocal<Person> personLocal = new ThreadLocal<>();
    private static final Random ran = new Random();
    private static final DateFormat format = new SimpleDateFormat("HH:mm:ss");

    /**
     * 不同的執行緒併發修改Person的age屬性
     */
    static class Wokrer implements Runnable{
        @Override
        public void run() {
            doExec();
        }

        private void doExec() {
            System.out.println(Thread.currentThread().getName() + " start task at "
                    + format.format(new Date()));
            //不同的執行緒會會將age屬性設定成不同的值
            int age = ran.nextInt(20);
            Person p = getPerson();
            //設定年齡
            p.setAge(age);
            System.out.println(Thread.currentThread().getName() + ": set age to " + p.getAge() + " at "
                + format.format(new Date()));
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ": get age " + p.getAge() + " at "
                + format.format(new Date()));
        }

        protected Person getPerson() {
            Person p = personLocal.get();
            if (p == null){
                p = new Person();
                personLocal.set(p);
            }
            return p;
        }
    }

    public static void main(String[] args){
        Wokrer wokrer = new Wokrer();
        new Thread(wokrer,"worker-1").start();
        new Thread(wokrer,"worker-2").start();
    }
}

執行結果如下:

執行結果

ThreadLocal小結

  1. ThreadLocal是指執行緒本地變數,不是指Thread
  2. ThreadLocal使用場合主要解決多執行緒中資料資料因併發產生不一致問題。也就是說如果想每個執行緒都在操作共享資料的時候不互相影響,但是又不想使用同步解決,那麼ThreadLocal會是你的菜
  3. ThreadLocal實現執行緒隔離的核心在於為每個訪問該值的執行緒都建立了一個ThreadLocalMap,這樣不同的執行緒在操作共享資料時可以不互相影響
  4. 與synchronized的區別:synchronized用於執行緒間的資料共享,而ThreadLocal則用於執行緒間的資料隔離。兩者使用的領域不同,ThreadLocal並不是為了替代synchronized而出現的,而且ThreadLocal不能實現原子性,因為ThreadLocal的ThreadLocalMap的操作實際的作用範圍是單執行緒,與多執行緒沒有任何關係
  5. 在多執行緒情況下使用ThreadLocal而建立的ThreadLocalMap是否會出現記憶體溢位:答案是不會。因為儲存資料的Entry是弱引用,執行緒執行結束後會自動被垃圾回收。