Java數據結構和算法系列3--ThreadLocal類原理詳解

1.ThreadLocal介紹

Java實現多線程的2種方式,繼承Thread類和實現Runnable接口。今天我們介紹下另外一種常用的多線程類ThreadLocal類。
ThreadLocal在維護變量時,爲每個使用變量的線程提供了獨立的副本,所以每個線程都可以獨立的改變自己的副本,而不影響其他線程對應的副本。

2.原理

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

void set(Object value)設置當前線程的線程局部變量的值。

public Object get()該方法返回當前線程所對應的線程局部變量。

public void remove()將當前線程局部變量的值刪除,目的是爲了減少內存的佔用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結束後,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量並不是必須的操作,但它可以加快內存回收的速度。

protected Object initialValue()返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是爲了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,並且僅執行1次。ThreadLocal中的缺省實現直接返回一個null。

  值得一提的是,在JDK5.0中,ThreadLocal已經支持泛型,該類的類名已經變爲ThreadLocal。API方法也相應進行了調整,新版本的API方法分別是void set(T value)、T get()以及T initialValue()。

  ThreadLocal是如何做到爲每一個線程維護變量的副本的呢?其實實現的思路很簡單:在ThreadLocal類中有一個Map,用於存儲每一個線程的變量副本,Map中元素的鍵爲線程對象,而值對應線程的變量副本。
  

3.Thread和ThreadLocal實例對比

//Thread實例

package com.tngtech.thread;

/*
 * 
 * @author tngtech
 * @date 2015年12月29日
 *<p>博客:http://blog.csdn.net/jacman
 *<p>Github:https://github.com/tangthis
 *
 */
public class ThreadDemo implements Runnable{

    private Integer i = 1;

    @Override
    public void run() {
        for(int j = 0 ; j < 10 ; j++){
            i = i + j;
        }
        System.out.println("變量值爲:" + i);
    }


    public static void main(String[] args) {
        ThreadDemo threadDemoRunnable = new ThreadDemo();
        Thread thread1 = new Thread(threadDemoRunnable);
        Thread thread2 = new Thread(threadDemoRunnable);
        thread1.start();

        //爲了看到更明顯的效果,線程睡眠1s,再啓動另外一個線程
        try{
            Thread.sleep(1000);
        }catch(Exception e){
            e.printStackTrace();
        }
        thread2.start();
        //打印結果
        //變量值爲:46
        //變量值爲:91
        //變量在多個線程間是共享的
    }
    }

在ThreadDemo中,我們修改變量i的值,根據輸出結果,發現i變量在多個線程間是共享的。下面看下ThreadLocal的實例:

/**
 * ThreadLocal實例
 */
package com.tngtech.thread;

/*
 * ThreadLocal實例
 * @author tngtech
 * @date 2015年12月29日
 *<p>博客:http://blog.csdn.net/jacman
 *<p>Github:https://github.com/tangthis
 *
 */
public class ThreadLocalDemo implements Runnable{
    //支持泛型
    ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 1;
        }
    };
    public void set(Integer i){
        local.set(i);
    }

    public Integer get(){
        return ((Integer)local.get()).intValue();
    }

    @Override
    public void run() {
        Integer i = get();
        for(int j = 0 ; j < 10; j ++){
            i = i + j;
        }
        set(i);


        System.out.println("變量值爲:" + i);
    }

    public static void main(String[] args) {
        ThreadLocalDemo threadRunnable = new ThreadLocalDemo();
        Thread thread1 = new Thread(threadRunnable);
        Thread thread2 = new Thread(threadRunnable);
        thread1.start();

        //爲了看到更明顯的效果,線程睡眠1s
        try{
            Thread.sleep(1000);
        }catch(Exception e){
            e.printStackTrace();
        }
        thread2.start();

        //打印結果
        //變量值爲:46
        //變量值爲:46
    }

}

通過ThreadLocalDemo我們可以看出,變量i在多個線程見是獨立的,互不影響。

4.同步機制

ThreadLocal和線程同步機制比較有什麼優勢呢?它們相同點都是爲了解決多線程中變量訪問衝突的問題。

在同步機制中,通過鎖機制來保證變量在同一個時間點只能被一個線程所持有。這個時候,該變量在多個線程間是共享的。使用同步機制,要求程序縝密的分析什麼時候對變量進行讀寫,什麼時候要鎖定對象,什麼時候需要釋放鎖等一系列問題,讓我們的程序變得複雜。

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

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

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

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

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

下面的實例能夠體現Spring對有狀態Bean的改造思路:
TestDao:非線程安全

package com.tngtech.thread;

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這個非線程安全的“狀態”進行改造:
TestDao:線程安全

package com.tngtech.thread;

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。

package com.tngtech.thread;

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);
    }
}

4.ThreadLocal具體實現

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

 /**
     * 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方法的實現:

    /**
     * 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()方法:

    /**
     * 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()方法:

 /**
     * 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;
    }

獲取和當前線程綁定的值時,ThreadLocalMap對象是以this指向的ThreadLocal對象爲鍵進行查找的,這當然和前面set()方法的代碼是相呼應的。

進一步地,我們可以創建不同的ThreadLocal實例來實現多個變量在不同線程間的訪問隔離,爲什麼可以這麼做?因爲不同的ThreadLocal對象作爲不同鍵,當然也可以在線程的ThreadLocalMap對象中設置不同的值了。通過ThreadLocal對象,在多線程中共享一個值和多個值的區別,就像你在一個HashMap對象中存儲一個鍵值對和多個鍵值對一樣,僅此而已。

5.總結

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

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