threadlocal原理分析

簡介

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路。使用這個工具類能夠很簡潔地編寫出優美的多線程程序。當使用ThreadLocal維護變量時,ThreadLocal爲每一個使用該變量的線程提供獨立的變量副本,因此每個線程均可以獨立地改變本身的副本,而不會影響其它線程所對應的副本。從線程的角度看,目標變量就象是線程的本地變量,這也是類名中「Local」所要表達的意思。因此,在Java中編寫線程局部變量的代碼相對來講要笨拙一些,所以形成線程局部變量沒有在Java開發者中獲得很好的普及。引自@楓之逆 人們常說,鎖是一種以時間換空間的機制,而ThreadLocal正好是以空間換時間的。 java


和鎖的比較

爲何要先強調這一點,由於從簡介上看,容易令人們聯想到ThreadLocal彷佛是一種解決Java多線程環境中線程同步與線程安全方法,其實否則,這裏要設計到兩個概念:線程安全,線程同步,事實上,ThreadLocal只解決線程安全的問題,並不能解決線程同步的問題,這也形成在剛學習的時候,我老是在苦想,ThreadLocal既然爲每一個線程拷貝一份變量,那怎麼再進行同步呢?查了不少資料後纔想明白,ThreadLocal並非用來解決線程同步的,因此它與鎖能夠說是沒有什麼關係的,彼此各有所長,不能代替。有人這樣描述:ThreadLocal解決的是同一個線程內的資源共享問題,而synchronized 解決的是多個線程間的資源共享問題,我以爲是有道理的!推薦讀者看一下討論貼,是關於synchronized關鍵字和ThreadLocal的,有興趣的能夠看一下,去 討論帖


如下是轉載自@楓之逆的博文: mysql

ThreadLocal類接口很簡單,只有4個方法,咱們先來了解一下: sql

  • void set(T value)設置當前線程的線程局部變量的值。
  • public T get()該方法返回當前線程所對應的線程局部變量。
  • public void remove()將當前線程局部變量的值刪除,目的是爲了減小內存的佔用,該方法是JDK 5.0新增的方法。須要指出的是,當線程結束後,對應該線程的局部變量將自動被垃圾回收,因此顯式調用該方法清除線程的局部變量並非必須的操做,但它能夠加快內存回收的速度。
  • protected T initialValue()返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是爲了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(T value)時才執行,而且僅執行1次。ThreadLocal中的缺省實現直接返回一個null。

值得一提的是,在JDK5.0中,ThreadLocal已經支持泛型,該類的類名已經變爲ThreadLocal<T>。API方法也相應進行了調整,新版本的API方法分別是void set(T value)、T get()以及T initialValue()。ThreadLocal是如何作到爲每個線程維護變量的副本的呢?其實實現的思路很簡單:在ThreadLocal類中有一個Map,用於存儲每個線程的變量副本,Map中元素的鍵爲線程對象,而值對應線程的變量副本。咱們本身就能夠提供一個簡單的實現版本: 安全

  1. package com.test;  
  2.   
  3. public class TestNum {  
  4.     // ①經過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值  
  5.     private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {  
  6.         public Integer initialValue() {  
  7.             return 0;  
  8.         }  
  9.     };  
  10.   
  11.     // ②獲取下一個序列值  
  12.     public int getNextNum() {  
  13.         seqNum.set(seqNum.get() + 1);  
  14.         return seqNum.get();  
  15.     }  
  16.   
  17.     public static void main(String[] args) {  
  18.         TestNum sn = new TestNum();  
  19.         // ③ 3個線程共享sn,各自產生序列號  
  20.         TestClient t1 = new TestClient(sn);  
  21.         TestClient t2 = new TestClient(sn);  
  22.         TestClient t3 = new TestClient(sn);  
  23.         t1.start();  
  24.         t2.start();  
  25.         t3.start();  
  26.     }  
  27.   
  28.     private static class TestClient extends Thread {  
  29.         private TestNum sn;  
  30.   
  31.         public TestClient(TestNum sn) {  
  32.             this.sn = sn;  
  33.         }  
  34.   
  35.         public void run() {  
  36.             for (int i = 0; i < 3; i++) {  
  37.                 // ④每一個線程打出3個序列值  
  38.                 System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["  
  39.                          + sn.getNextNum() + "]");  
  40.             }  
  41.         }  
  42.     }  
  43. }  
package com.test;

public class TestNum {
	// ①經過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值
	private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
		public Integer initialValue() {
			return 0;
		}
	};

	// ②獲取下一個序列值
	public int getNextNum() {
		seqNum.set(seqNum.get() + 1);
		return seqNum.get();
	}

	public static void main(String[] args) {
		TestNum sn = new TestNum();
		// ③ 3個線程共享sn,各自產生序列號
		TestClient t1 = new TestClient(sn);
		TestClient t2 = new TestClient(sn);
		TestClient t3 = new TestClient(sn);
		t1.start();
		t2.start();
		t3.start();
	}

	private static class TestClient extends Thread {
		private TestNum sn;

		public TestClient(TestNum sn) {
			this.sn = sn;
		}

		public void run() {
			for (int i = 0; i < 3; i++) {
				// ④每一個線程打出3個序列值
				System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["
						 + sn.getNextNum() + "]");
			}
		}
	}
}

一般咱們經過匿名內部類的方式定義ThreadLocal的子類,提供初始的變量值,如例子中①處所示。TestClient線程產生一組序列號,在③處,咱們生成3個TestClient,它們共享同一個TestNum實例。運行以上代碼,在控制檯上輸出如下的結果: 多線程

thread[Thread-0] --> sn[1]
thread[Thread-1] --> sn[1]
thread[Thread-2] --> sn[1]
thread[Thread-1] --> sn[2]
thread[Thread-0] --> sn[2]
thread[Thread-1] --> sn[3]
thread[Thread-2] --> sn[2]
thread[Thread-0] --> sn[3]
thread[Thread-2] --> sn[3]

考察輸出的結果信息,咱們發現每一個線程所產生的序號雖然都共享同一個TestNum實例,但它們並無發生相互干擾的狀況,而是各自產生獨立的序列號,這是由於咱們經過ThreadLocal爲每個線程提供了單獨的副本。 併發

Thread同步機制的比較

ThreadLocal和線程同步機制相比有什麼優點呢?ThreadLocal和線程同步機制都是爲了解決多線程中相同變量的訪問衝突問題。在同步機制中,經過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序慎密地分析何時對變量進行讀寫,何時須要鎖定某個對象,何時釋放對象鎖等繁雜的問題,程序設計和編寫難度相對較大。而ThreadLocal則從另外一個角度來解決多線程的併發訪問。ThreadLocal會爲每個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問衝突。由於每個線程都擁有本身的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,能夠把不安全的變量封裝進ThreadLocal。因爲ThreadLocal中能夠持有任何類型的對象,低版本JDK所提供的get()返回的是Object對象,須要強制類型轉換。但JDK 5.0經過泛型很好的解決了這個問題,在必定程度地簡化ThreadLocal的使用,代碼清單 9 2就使用了JDK 5.0新的ThreadLocal<T>版本。 ide


歸納起來講,對於多線程資源共享的問題,同步機制採用了「以時間換空間」的方式,而ThreadLocal採用了「以空間換時間」的方式。前者僅提供一份變量,讓不一樣的線程排隊訪問,然後者爲每個線程都提供了一份變量,所以能夠同時訪問而互不影響。Spring使用ThreadLocal解決線程安全問題咱們知道在通常狀況下,只有無狀態的Bean才能夠在多線程環境下共享,在Spring中,絕大部分Bean均可以聲明爲singleton做用域。就是由於Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全狀態採用ThreadLocal進行處理,讓它們也成爲線程安全的狀態,由於有狀態的Bean就能夠在多線程中共享了。通常的Web應用劃分爲展示層、服務層和持久層三個層次,在不一樣的層中編寫對應的邏輯,下層經過接口向上層開放功能調用。在通常狀況下,從接收請求到返回響應所通過的全部程序調用都同屬於一個線程,以下圖所示: 工具


同一線程貫通三層這樣你就能夠根據須要,將一些非線程安全的變量以ThreadLocal存放,在同一次請求響應的調用線程中,全部關聯的對象引用到的都是同一個變量。下面的實例可以體現Spring對有狀態Bean的改造思路:代碼清單3 TestDao:非線程安全 學習

  1. package com.test;  
  2.   
  3. import java.sql.Connection;  
  4. import java.sql.SQLException;  
  5. import java.sql.Statement;  
  6.   
  7. public class TestDao {  
  8.     private Connection conn;// ①一個非線程安全的變量  
  9.   
  10.     public void addTopic() throws SQLException {  
  11.         Statement stat = conn.createStatement();// ②引用非線程安全變量  
  12.         // …  
  13.     }  
  14. }  
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:線程安全 this

  1. package com.test;  
  2.   
  3. import java.sql.Connection;  
  4. import java.sql.SQLException;  
  5. import java.sql.Statement;  
  6.   
  7. public class TestDaoNew {  
  8.     // ①使用ThreadLocal保存Connection變量  
  9.     private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();  
  10.   
  11.     public static Connection getConnection() {  
  12.         // ②若是connThreadLocal沒有本線程對應的Connection建立一個新的Connection,  
  13.         // 並將其保存到線程本地變量中。  
  14.         if (connThreadLocal.get() == null) {  
  15.             Connection conn = getConnection();  
  16.             connThreadLocal.set(conn);  
  17.             return conn;  
  18.         } else {  
  19.             return connThreadLocal.get();// ③直接返回線程本地變量  
  20.         }  
  21.     }  
  22.   
  23.     public void addTopic() throws SQLException {  
  24.         // ④從ThreadLocal中獲取線程對應的Connection  
  25.         Statement stat = getConnection().createStatement();  
  26.     }  
  27. }  
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 = new ThreadLocal<Connection>();

	public static Connection getConnection() {
		// ②若是connThreadLocal沒有本線程對應的Connection建立一個新的Connection,
		// 並將其保存到線程本地變量中。
		if (connThreadLocal.get() == null) {
			Connection conn = getConnection();
			connThreadLocal.set(conn);
			return conn;
		} else {
			return connThreadLocal.get();// ③直接返回線程本地變量
		}
	}

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

不一樣的線程在使用TopicDao時,先判斷connThreadLocal.get()是不是null,若是是null,則說明當前線程尚未對應的Connection對象,這時建立一個Connection對象並添加到本地線程變量中;若是不爲null,則說明當前的線程已經擁有了Connection對象,直接使用就能夠了。這樣,就保證了不一樣的線程使用線程相關的Connection,而不會使用其它線程的Connection。所以,這個TopicDao就能夠作到singleton共享了。固然,這個例子自己很粗糙,將Connection的ThreadLocal直接放在DAO只能作到本DAO的多個方法共享Connection時不發生線程安全問題,但沒法和其它DAO共用同一個Connection,要作到同一事務多DAO共享同一Connection,必須在一個共同的外部類使用ThreadLocal保存Connection。ConnectionManager.java

  1. package com.test;  
  2.   
  3. import java.sql.Connection;  
  4. import java.sql.DriverManager;  
  5. import java.sql.SQLException;  
  6.   
  7. public class ConnectionManager {  
  8.   
  9.     private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
  10.         @Override  
  11.         protected Connection initialValue() {  
  12.             Connection conn = null;  
  13.             try {  
  14.                 conn = DriverManager.getConnection(  
  15.                         "jdbc:mysql://localhost:3306/test""username",  
  16.                         "password");  
  17.             } catch (SQLException e) {  
  18.                 e.printStackTrace();  
  19.             }  
  20.             return conn;  
  21.         }  
  22.     };  
  23.   
  24.     public static Connection getConnection() {  
  25.         return connectionHolder.get();  
  26.     }  
  27.   
  28.     public static void setConnection(Connection conn) {  
  29.         connectionHolder.set(conn);  
  30.     }  
  31. }  
package com.test;

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

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<T>

那麼到底ThreadLocal類是如何實現這種「爲每一個線程提供不一樣的變量拷貝」的呢?先來看一下ThreadLocal的set()方法的源碼是如何實現的:

  1. /** 
  2.     * Sets the current thread's copy of this thread-local variable 
  3.     * to the specified value.  Most subclasses will have no need to 
  4.     * override this method, relying solely on the {@link #initialValue} 
  5.     * method to set the values of thread-locals. 
  6.     * 
  7.     * @param value the value to be stored in the current thread's copy of 
  8.     *        this thread-local. 
  9.     */  
  10.    public void set(T value) {  
  11.        Thread t = Thread.currentThread();  
  12.        ThreadLocalMap map = getMap(t);  
  13.        if (map != null)  
  14.            map.set(this, value);  
  15.        else  
  16.            createMap(t, value);  
  17.    }  
/**
     * 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);
    }

在這個方法內部咱們看到,首先經過getMap(Thread t)方法獲取一個和當前線程相關的ThreadLocalMap,而後將變量的值設置到這個ThreadLocalMap對象中,固然若是獲取到的ThreadLocalMap對象爲空,就經過createMap方法建立。線程隔離的祕密,就在於ThreadLocalMap這個類。ThreadLocalMap是ThreadLocal類的一個靜態內部類,它實現了鍵值對的設置和獲取(對比Map對象來理解),每一個線程中都有一個獨立的ThreadLocalMap副本,它所存儲的值,只能被當前線程讀取和修改。ThreadLocal類經過操做每個線程特有的ThreadLocalMap副本,從而實現了變量訪問在不一樣線程中的隔離。由於每一個線程的變量都是本身特有的,徹底不會有併發錯誤。還有一點就是,ThreadLocalMap存儲的鍵值對中的鍵是this對象指向的ThreadLocal對象,而值就是你所設置的對象了。爲了加深理解,咱們接着看上面代碼中出現的getMap和createMap方法的實現:
  1. /** 
  2.  * Get the map associated with a ThreadLocal. Overridden in 
  3.  * InheritableThreadLocal. 
  4.  * 
  5.  * @param  t the current thread 
  6.  * @return the map 
  7.  */  
  8. ThreadLocalMap getMap(Thread t) {  
  9.     return t.threadLocals;  
  10. }  
  11.   
  12. /** 
  13.  * Create the map associated with a ThreadLocal. Overridden in 
  14.  * InheritableThreadLocal. 
  15.  * 
  16.  * @param t the current thread 
  17.  * @param firstValue value for the initial entry of the map 
  18.  * @param map the map to store. 
  19.  */  
  20. void createMap(Thread t, T firstValue) {  
  21.     t.threadLocals = new ThreadLocalMap(this, firstValue);  
  22. }  
/**
     * 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類中的get()方法:
  1. /** 
  2.  * Returns the value in the current thread's copy of this 
  3.  * thread-local variable.  If the variable has no value for the 
  4.  * current thread, it is first initialized to the value returned 
  5.  * by an invocation of the {@link #initialValue} method. 
  6.  * 
  7.  * @return the current thread's value of this thread-local 
  8.  */  
  9. public T get() {  
  10.     Thread t = Thread.currentThread();  
  11.     ThreadLocalMap map = getMap(t);  
  12.     if (map != null) {  
  13.         ThreadLocalMap.Entry e = map.getEntry(this);  
  14.         if (e != null)  
  15.             return (T)e.value;  
  16.     }  
  17.     return setInitialValue();  
  18. }  
/**
     * 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)
                return (T)e.value;
        }
        return setInitialValue();
    }

再來看setInitialValue()方法:
  1. /** 
  2.     * Variant of set() to establish initialValue. Used instead 
  3.     * of set() in case user has overridden the set() method. 
  4.     * 
  5.     * @return the initial value 
  6.     */  
  7.    private T setInitialValue() {  
  8.        T value = initialValue();  
  9.        Thread t = Thread.currentThread();  
  10.        ThreadLocalMap map = getMap(t);  
  11.        if (map != null)  
  12.            map.set(this, value);  
  13.        else  
  14.            createMap(t, value);  
  15.        return value;  
  16.    }  
相關文章
相關標籤/搜索