深挖ThreadLocal

先總述,後分析

  深挖過threadLocal之後,一句話概括:Synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離。所以ThreadLocal的應用場合,最適合的是按線程多實例(每個線程對應一個實例)的對象的訪問,並且這個對象很多地方都要用到。

  數據隔離的祕訣其實是這樣的,Thread有個TheadLocalMap類型的屬性,叫做threadLocals,該屬性用來保存該線程本地變量。這樣每個線程都有自己的數據,就做到了不同線程間數據的隔離,保證了數據安全。

  接下來採用jdk1.8源碼進行深挖一下TheadLocal和TheadLocalMap。

ThreadLocal是什麼

  早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多線程程序。

  當使用ThreadLocal維護變量時,ThreadLocal爲每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。

  從線程的角度看,目標變量就象是線程的本地變量,這也是類名中“Local”所要表達的意思。

  所以,在Java中編寫線程局部變量的代碼相對來說要笨拙一些,因此造成線程局部變量沒有在Java開發者中得到很好的普及。

 

原理

  ThreadLocal,連接ThreadLocalMap和Thread。來處理Thread的TheadLocalMap屬性,包括init初始化屬性賦值、get對應的變量,set設置變量等。通過當前線程,獲取線程上的ThreadLocalMap屬性,對數據進行get、set等操作。

  ThreadLocalMap,用來存儲數據,採用類似hashmap機制,存儲了以threadLocal爲key,需要隔離的數據爲value的Entry鍵值對數組結構。

  ThreadLocal,有個ThreadLocalMap類型的屬性,存儲的數據就放在這兒。

ThreadLocal、ThreadLocal、Thread之間的關係

  ThreadLocalMap是ThreadLocal內部類,由ThreadLocal創建,Thread有ThreadLocal.ThreadLocalMap類型的屬性。源碼如下:

Thread的屬性:

public
class Thread implements Runnable {
    /*...其他屬性...*/

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

ThreadLocal和ThreadLocalMap

public class ThreadLocal<T> {
    /**..其他屬性和方法稍後介紹...*/
    /**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    static class ThreadLocalMap {

由ThreadLocal對Thread的TreadLocalMap進行賦值

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

ThreadLocal的接口方法

ThreadLocal類核心方法set、get、initialValue、withInitial、setInitialValue、remove:

   /**
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the {@code initialValue} method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * <p>This implementation simply returns {@code null}; if the
     * programmer desires thread-local variables to have an initial
     * value other than {@code null}, {@code ThreadLocal} must be
     * subclassed, and this method overridden.  Typically, an
     * anonymous inner class will be used.
     *
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }

    /**
     * Creates a thread local variable. The initial value of the variable is
     * determined by invoking the {@code get} method on the {@code Supplier}.
     *
     * @param <S> the type of the thread local's value
     * @param supplier the supplier to be used to determine the initial value
     * @return a new thread local variable
     * @throws NullPointerException if the specified supplier is null
     * @since 1.8
     */
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

    /**
     * Creates a thread local variable.
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    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();
    }

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    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;
    }

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
  • initialValue返回該線程局部變量的初始值。該方法是一個protected的方法,顯然是爲了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,並且僅執行1次。ThreadLocal中的缺省實現直接返回一個null。
  • withInitial提供一個Supplier的lamda表達式用來當做初始值,java8引入。
  • setInitialValue設置初始值。在get操作沒有對應的值時,調用此方法。private方法,防止被覆蓋。過程和set類似,只不過是用initialValue作爲value進行設置。
  • set設置當前線程對應的線程局部變量的值。先取出當前線程對應的threadLocalMap,如果不存在則用創建一個,否則將value放入以this,即threadLocal爲key的映射的map中,其實threadLocalMap內部和hashMap機制一樣,存儲了Entry鍵值對數組,後續會深挖threadLocalMap。
  • get該方法返回當前線程所對應的線程局部變量。和set類似,也是先取出當前線程對應的threadLocalMap,如果不存在則用創建一個,但是是用inittialValue作爲value放入到map中,且返回initialValue,否則就直接從map取出this即threadLocal對應的value返回。
  • remove將當前線程局部變量的值刪除,目的是爲了減少內存的佔用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結束後,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量並不是必須的操作,但它可以加快內存回收的速度。需要注意的是如果remove之後又調用了get,會重新初始化一次,即再次調用initialValue方法,除非在get之前調用set設置過值。

ThreadLocalMap簡介

  看名字就知道是個map,沒錯,這就是個hashMap機制實現的map,用Entry數組來存儲鍵值對,key是ThreadLocal對象,value則是具體的值。值得一提的是,爲了方便GC,Entry繼承了WeakReference,也就是弱引用。裏面有一些具體關於如何清理過期的數據、擴容等機制,思路基本和hashmap差不多,有興趣的可以自行閱讀了解,這邊只需知道大概的數據存儲結構即可。

    /**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

 

Thread同步機制的比較

 

  ThreadLocal和線程同步機制相比有什麼優勢呢?

  Synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離。

  在同步機制中,通過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序慎密地分析什麼時候對變量進行讀寫,什麼時候需要鎖定某個對象,什麼時候釋放對象鎖等繁雜的問題,程序設計和編寫難度相對較大。

  而ThreadLocal則從另一個角度來解決多線程的併發訪問。ThreadLocal會爲每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問衝突。因爲每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal。

  概括起來說,對於多線程資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而後者爲每一個線程都提供了一份變量,因此可以同時訪問而互不影響。

  Spring使用ThreadLocal解決線程安全問題我們知道在一般情況下,只有無狀態的Bean纔可以在多線程環境下共享,在Spring中,絕大部分Bean都可以聲明爲singleton作用域。就是因爲Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全狀態採用ThreadLocal進行處理,讓它們也成爲線程安全的狀態,因爲有狀態的Bean就可以在多線程中共享了。

  一般的Web應用劃分爲展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過接口向上層開放功能調用。在一般情況下,從接收請求到返回響應所經過的所有程序調用都同屬於一個線程。

 

  同一線程貫通三層這樣你就可以根據需要,將一些非線程安全的變量以ThreadLocal存放,在同一次請求響應的調用線程中,所有關聯的對象引用到的都是同一個變量。

  下面的實例能夠體現Spring對有狀態Bean的改造思路:

代碼清單3 TestDao:非線程安全

 

package com.test;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

public class TestDao {
	private Connection conn;// ①一個非線程安全的變量

	public void addTopic() throws SQLException {
		Statement stat = conn.createStatement();// ②引用非線程安全變量
		// …
	}
}

 

 

 

由於①處的conn是成員變量,因爲addTopic()方法是非線程安全的,必須在使用時創建一個新TopicDao實例(非singleton)。下面使用ThreadLocal對conn這個非線程安全的“狀態”進行改造:

代碼清單4 TestDao:線程安全

 

package com.test;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

public class TestDaoNew {// ①使用ThreadLocal保存Connection變量
  private static ThreadLocal<Connection> connThreadLocal = ThreadLocal.withInitial(Test::createConnection);

  // 具體創建數據庫連接的方法
  private static Connection createConnection() {
    Connection result = null;
    /**
     * create a real connection...
     * such as :
     * result = DriverManager.getConnection(dbUrl, dbUser, dbPwd);
     */
    return result;
  }

  // ③直接返回線程本地變量
  public static Connection getConnection() {
    return connThreadLocal.get();
  }

  // 具體操作
  public void addTopic() throws SQLException {
    // ④從ThreadLocal中獲取線程對應的Connection
    Statement stat = getConnection().createStatement();
    //....any other operation
  }
}

 

  不同的線程在使用TopicDao時,根據之前的深挖get具體操作,判斷connThreadLocal.get()會去判斷是有map,沒有則根據initivalValue創建一個Connection對象並添加到本地線程變量中,initivalValue對應的值也就是上述的lamba表達式對應的創建connection的方法返回的結果,下次get則由於已經有了,則會直接獲取已經創建好的Connection,這樣,就保證了不同的線程使用線程相關的Connection,而不會使用其它線程的Connection。因此,這個TopicDao就可以做到singleton共享了。

 

  當然,這個例子本身很粗糙,將Connection的ThreadLocal直接放在DAO只能做到本DAO的多個方法共享Connection時不發生線程安全問題,但無法和其它DAO共用同一個Connection,要做到同一事務多DAO共享同一Connection,必須在一個共同的外部類使用ThreadLocal保存Connection。


 

ConnectionManager.java

package com.test;

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

public class ConnectionManager {

	private static ThreadLocal<Connection> connectionHolder = ThreadLocal.withInitial(() -> {
		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();
	}
}

 

線程隔離的祕密

 

祕密就就在於上述敘述的ThreadLocalMap這個類。ThreadLocalMap是ThreadLocal類的一個靜態內部類,它實現了鍵值對的設置和獲取(對比Map對象來理解),每個線程中都有一個獨立的ThreadLocalMap副本,它所存儲的值,只能被當前線程讀取和修改。ThreadLocal類通過操作每一個線程特有的ThreadLocalMap副本,從而實現了變量訪問在不同線程中的隔離。因爲每個線程的變量都是自己特有的,完全不會有併發錯誤。還有一點就是,ThreadLocalMap存儲的鍵值對中的鍵是this對象指向的ThreadLocal對象,而值就是你所設置的對象了。


爲了加深理解,我們接着看上面代碼中出現的getMap和createMap方法的實現:

 

 

 

 

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     * @param map the map to store.
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

 

 

 

小結

  ThreadLocal是解決線程安全問題一個很好的思路,它通過爲每個線程提供一個獨立的變量副本解決了變量併發訪問的衝突問題。在很多情況下,ThreadLocal比直接使用synchronized同步機制解決線程安全問題更簡單,更方便,且結果程序擁有更高的併發性。

 

後記

  看到網友評論的很激烈,甚至關於ThreadLocalMap不是ThreadLocal裏面的,而是Thread裏面的這種評論都出現了,於是有了這個後記,下面先把jdk源碼貼上,源碼最有說服力了。

 

/**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    static class ThreadLocalMap {...}

  源碼就是以上,這源碼自然是在ThreadLocal裏面的,有截圖爲證。

 

  本文是自己在學習ThreadLocal的時候,一時興起,深入看了源碼,思考了此類的作用、使用範圍,進而聯想到對傳統的synchronize共享變量線程安全的問題進行比較,而總結的博文,總結一句話就是一個是鎖機制進行時間換空間,一個是存儲拷貝進行空間換時間。

 

(全文完)

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章