JAVA多線程-ThreadLocal線程本地存儲

一、關于線程本地存儲

線程本地存儲是一種自動化機制,可以為使用相同變量的每個不同的線程都創(chuàng)建不同的存儲,通過根除對變量的共享來防止任務在共享資源時產生沖突。

因此,如果你有5個線程都要使用變量x所表示的對象,那么線程本地存儲就會生成5個用于x的不同的存儲塊,并且使得你可以將狀態(tài)與線程關聯起來。

二、ThreadLocal是什么?

ThreadLocal是線程本地存儲的一種實現方案。它并不是一個Thread,我們也可以稱之為線程局部變量,在多線程并發(fā)訪問時,ThreadLocal類為每個使用該變量的線程都創(chuàng)建一個變量值的副本,每一個線程都可以獨立地改變自己的副本,而不會和其他線程的副本發(fā)生沖突。從線程的角度來看,就感覺像是每個線程都完全擁有該變量一樣。

三、什么情況下使用ThreadLocal?

ThreadLocal的使用場合主要用來解決多線程情況下對數據的讀取因線程并發(fā)而產生數據不一致的問題。ThreadLocal為每個線程中并發(fā)訪問的數據提供一個本地副本,然后通過對這個本地副本的訪問來執(zhí)行具體的業(yè)務邏輯操作,這樣就可以大大減少線程并發(fā)控制的復雜度;然而這樣做也需要付出一定的代價,需要耗費一部分內存資源,但是相比于線程同步所帶來的性能消耗還是要好上那么一點點。

四、ThreadLocal的應用場景

最常見的ThreadLocal的使用場景是用來解決數據庫連接、Session管理等等。如:

4.1、 數據庫連接管理:

同一事務多DAO共享同一Connection,必須在一個共同的外部類中使用threadLocal保存Connection。

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

通過上面這種方式就保證了一個線程對應一個數據庫連接,保證了事務。因為一般事務都是依賴一個個數據庫連接來控制的,如commit,rollback等都是需要獲取數據庫連接來操作的。

4.2、session管理:

private static final ThreadLocal threadSession = new ThreadLocal();

public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}

五、如何使用ThreadLocal

直接看代碼:

package com.feizi.java.concurrency.tool;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * Created by feizi on 2018/5/29.
 */
public class ThreadLocalHolder {
    private static ThreadLocal<Integer> holder = new ThreadLocal<Integer>(){
        private Random rand = new Random(10);
        protected synchronized Integer initialValue(){
            return rand.nextInt(100);
        }
    };

    public static void increment(){
        holder.set(holder.get() + 1);
    }

    public static Integer get(){
        return holder.get();
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++){
            threadPool.execute(new Accessor(i));
        }
        threadPool.shutdown();
    }
}

class Accessor implements Runnable{
    private final int id;

    public Accessor(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()){
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ThreadLocalHolder.increment();
            System.out.println(this);
            Thread.yield();
        }
    }

    @Override
    public String toString() {
        return "#" + id + " : " + ThreadLocalHolder.get();
    }
}

控制臺輸出結果:

#線程#3 : 14
#線程#0 : 81
#線程#4 : 94
#線程#2 : 91
#線程#1 : 47
#線程#1 : 48
#線程#4 : 95
#線程#3 : 15
#線程#2 : 92
#線程#0 : 82
#線程#3 : 16
#線程#0 : 83
#線程#4 : 96
#線程#2 : 93
#線程#1 : 49
#線程#2 : 94
#線程#3 : 17

從上面輸出結果,我們看到:每個線程的輸出的結果都是隔離的,相互并不影響,#線程#3首次輸出14,到了下次再輸出的時候變成15,#線程#0首次輸出81,再次輸出82,其他類似。

因為每個單獨的線程都被分配了自己的存儲,因為它們每個都需要跟蹤自己的計數值,即便只有一個ThreadLocalHolder對象。

六、ThreadLocal的實現

ThreadLocal中主要提供的方法:

1、public T get(){}

主要用于獲取ThreadLocal在當前線程中保存的變量副本

2、public void set(T value) {}

主要用于設置當前線程中變量的副本

3、public void remove() {}

主要用于移除當前線程中變量的副本

4、protected T initialValue() {}

它是一個被protected修飾的方法,主要用于在實例化時進行重載的,是一個延時加載方法

6.1、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();
}

首先獲取當前線程t,然后根據當前線程t調用getMap(t)方法獲取一個ThreadLocalMap類型的map,之后判斷這個map是否為空,如果不為空,則根據this(表示當前ThreadLocal對象)獲取一個<key,value>鍵值對的的Entry,需要注意的是,這里傳入的this,而不是當前線程t,如果map為空,則調用setInitialValue()初始化一個value,默認是返回null。

然后,我們跟一下getMap(t)中做了什么操作:

ThreadLocalMap getMap(Thread t) {
    //返回當前線程t中的一個成員變量threadLocals
    return t.threadLocals;
}

從上面可以看到,getMap(t)中返回了當前線程t中的一個成員變量threadLocals,接著再繼續(xù)往下跟,看一下threadLocals是什么東西:

ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到,threadLocals實際就是一個ThreadLocalMap,這個類是ThreadLocald的一個內部類,然后我們再看一下ThreadLocalMapd的定義:

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

    /**
        * The initial capacity -- MUST be a power of two.
        */
    private static final int INITIAL_CAPACITY = 16;

    /**
        * The table, resized as necessary.
        * table.length MUST always be a power of two.
        */
    private Entry[] table;

    /**
        * The number of entries in the table.
        */
    private int size = 0;

    /**
        * The next size value at which to resize.
        */
    private int threshold; // Default to 0
}

從上面的定義我們大致可以看出,ThreadLocalMap的鍵值對Entry類繼承了WeakReference,并且使用ThreadLocal<?>作為key值進行存儲。

6.2、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;
}

首先,調用initialValue()進行初始化value值,我們跟一下這個initialValue()方法:

protected T initialValue() {
    return null;
}

從上述initialValue()方法中,我們可以看到直接return返回了一個null,獲取當前線程t,根據當前線程t獲取ThreadLocalMap類型的map,此時再判斷map是否為空,不為空則直接設置<key,value>鍵值對,注意此處的key仍然還是this(表示當前threadLocal對象),為空則調用createMap(Thread t, T firstValue)方法:

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

從上面代碼我們可以看出,直接new了一個ThreadLocalMap對象,以this(當前threadLocal對象)作為key,傳入的value設置為值,并且賦給當前線程t的成員變量threadLocals。

6.3、set()方法的實現:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

首先獲取當前線程t,然后根據當前線程t獲取ThreadLocalMap類型的map,判斷map不為空就設置鍵值對,為空就調用createMap初始化一個新的map。

6.4、remove()方法的實現:

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

首先獲取當前線程t,根據當前線程t獲取ThreadLocalMap,判斷不為空,就根據this(當前threadLocal對象)移除相對應的value。

通過上面的分析,我們就大致明白了ThreadLocal的基本工作原理:

首先,每個線程Thread內部都擁有一個threadLocals變量(這個是在Thread類中定義的),這個threadLocals是ThreadLocal.ThreadLocalMap類型的,也就是一個Map,這個map是整個threadLocal得以實現的核心,它用于存儲變量的副本。key值為this(即當前threadLocal對象),value為變量副本(T類型的變量)。

當我們new一個ThreadLocal對象時,即初始化ThreadLocal),這個Thread類的threadLocals為null,然后進行get()或者set()的時候,都需要對這個Thread類的threadLocals進行初始化操作(步驟都是先獲取當前線程t,然后根據t獲取ThreadLocalMap,判斷如果為空,就初始化new一個),然后以this(當前ThreadLocal變量)為key,以threadLocal需要保存的副本變量為value,存到Thread類的threadLocals中。之后,在線程里面,如果需要使用變量副本,就可以通過get()方法,根據當前線程t去獲取threadLocals中對應存儲的value副本值。

ok,上面還是有些啰嗦,我們再總結一下:

  1. Thread類中定義了一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,用于保存變量的副本
  2. ThreadLocal類中定義了一個ThreadLocalMap的靜態(tài)內部類
  3. ThreadLocalMap類中定義了一個繼承WeakReference類的Entry鍵值對,并且這個Entry鍵值對有些特殊,特殊之處就在于它的key必須是ThreadLocal類型的
  4. threadLocals在保存變量副本的時候,以this(當前ThreadLocal變量)為key,以傳入需要保存的變量副本為value進行存儲
  5. ThreadLocal在get()的時候,會先獲取當前線程t,然后根據t去獲取ThreadLocalMap,之后對這個ThreadLocalMap進行判空,如果不為空,則根據this(當前ThreadLocal變量)獲取ThreadLocalMap類的Entry鍵值對,再對Entry鍵值對進行判空,如果不為空就取出變量副本value進行return,如果ThreadLocalMap為空,就調用setInitialValue()方法就行初始化,并且返回一個null的value默認值。
  6. ThreadLocal在set()的時候,也會先獲取當前線程t,然后根據t去獲取一個ThreadLocalMap,之后對這個ThreadLocalMap進行判空,如果不為空,就以this(當前ThreadLocal變量)為key,需要保存的變量副本為value設置鍵值對,否則就調用createMap初始化一個ThreadLocalMap。
  7. ThreadLocal在setInitialValue()的時候,同上面的set()過程類似,唯一的區(qū)別是setInitialValue()方法會返回一個默認值為null的value(需要對value初始化)
  8. ThreadLocal在remove()的時候,同樣也會先獲取當前線程t,然后根據t獲取一個ThreadLocalMap,之后再對這個ThreadLocalMap進行判空,如果不為空,則根據this(當前ThreadLocal變量)移除對應存儲的變量副本value。

其實簡單來說,大致就是每個線程都維護了一個map,而這個map的key就是當前threadLocal變量,而值則是我們需要set的那個變量的value,之后每次線程在get取值的時候都是從自己的變量中取值,既然是從自己的變量中取值,那么當然也就不存在線程安全的問題了。ThreadLocal只是充當一個key的角色,然后順帶給每個線程提供一個初始值。

多線程安全性解決方案

  1. 采用synchronized進行同步控制,但是效率略低,使得并發(fā)變同步(串行)
  2. 采用ThreadLocal線程本地存儲,為每個使用該變量的線程都存儲一個本地變量副本(線程互不相干)

兩種線程安全方案的區(qū)別

  1. synchronized同步機制采用了“以時間換空間”的方式,僅僅只提供一份變量,讓參與的多個不同的線程排隊進行訪問
  2. ThreadLocal采用“以空間換時間”的方式,為參與的每個線程都各自提供一份本地副本,因此可以做到同時訪問而互不影響。

綜上所述,ThreadLocal通常占用內存較大,但是速度快;而synchronized則占用內存小,速度相對而言比較慢。如果在內存比較充足的情況,對并發(fā)部分的執(zhí)行效率要求很高的話,那么就是ThreadLocal派上用場的時候了,一般情況下還是synchronized用的居多。

原文參考

  1. http://www.cnblogs.com/dolphin0520/p/3920407.html
  2. https://www.cnblogs.com/zhangjk1993/archive/2017/03/29/6641745.html#_label4
  3. https://www.cnblogs.com/xinxin-ting/p/7070826.html
  4. https://blog.csdn.net/sean417/article/details/69948561
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容