ThreadLocal 理解與應(yīng)用

ThreadLocal 理解與應(yīng)用

在并發(fā)編程中,我們主要考慮的問(wèn)題是多個(gè)線(xiàn)程對(duì)于共享數(shù)據(jù)的訪(fǎng)問(wèn),并在訪(fǎng)問(wèn)共享數(shù)據(jù)時(shí)保證線(xiàn)程安全。如果我們希望每個(gè)線(xiàn)程都有一個(gè)共享變量的副本,并且對(duì)這個(gè)副本進(jìn)行讀寫(xiě)時(shí)不影響其他的線(xiàn)程該如何做呢?

JDK 為我們提供了ThreadLocal類(lèi)來(lái)解決線(xiàn)程與數(shù)據(jù)綁定的需求。如果說(shuō)synchronizedvolatile關(guān)鍵字保證了線(xiàn)程間的數(shù)據(jù)共享(可見(jiàn)性),那么ThraedLocal類(lèi)就是保證線(xiàn)程間的數(shù)據(jù)隔離。為什么這么說(shuō)呢?

volatilesynchronized保證共享數(shù)據(jù)在不同的線(xiàn)程中是可見(jiàn)的,一個(gè)線(xiàn)程對(duì)共享數(shù)據(jù)的改變其他線(xiàn)程也能觀察到,通過(guò)同步機(jī)制來(lái)保證線(xiàn)程安全。而ThreadLocal類(lèi)則提供了另一個(gè)保證線(xiàn)程安全的處理思路:

每個(gè)線(xiàn)程持有共享變量的一個(gè)副本,并且與線(xiàn)程綁定,這樣每個(gè)線(xiàn)程對(duì)這個(gè)變量的讀寫(xiě)都在自己線(xiàn)程內(nèi)部,對(duì)線(xiàn)程來(lái)說(shuō),這個(gè)變量是屬于線(xiàn)程私有的,不會(huì)對(duì)其他線(xiàn)程有影響,避免出現(xiàn)數(shù)據(jù)不一致的情況,也就是線(xiàn)程間的數(shù)據(jù)是隔離的,互不相干的。

什么是線(xiàn)程局部變量(thread-local variable)?

線(xiàn)程局部變量就是為每一個(gè)使用該變量的線(xiàn)程都提供一個(gè)變量值的副本,該副本是線(xiàn)程私有的,不同的線(xiàn)程持有不同的副本,每個(gè)線(xiàn)程都可以獨(dú)立的改變這個(gè)副本并且不會(huì)和其他的線(xiàn)程起沖突。這是 Java 中較為特殊的線(xiàn)程綁定機(jī)制,從而為多線(xiàn)程環(huán)境中常出現(xiàn)的并發(fā)訪(fǎng)問(wèn)問(wèn)題提供了一種數(shù)據(jù)隔離機(jī)制。

如何使用 ThreadLocal

  1. 創(chuàng)建 ThreadLocal 實(shí)例

        public static ThreadLocal<Integer> threadLocalData = new ThreadLocal<>();
        public static void main(String[] args) {
        System.out.println(threadLocalData.get());//輸出為Null
    }
    

    一般將 ThreadLocal 變量聲明為公開(kāi)的靜態(tài)字段,方便線(xiàn)程對(duì)其進(jìn)行訪(fǎng)問(wèn),需要注意的是:直接使用構(gòu)造方法聲明 ThreadLocal 對(duì)象后,對(duì)象的初始值為 null。

  2. ThreadLocal 對(duì)象初始化值
    ThreadLocal類(lèi)提供了初始化接口

    protected T initialValue()
    

    我們可以通過(guò)繼承 ThreadLocal 類(lèi)并復(fù)寫(xiě)initialValue()方法提供初始值,提供的初始值對(duì)所有線(xiàn)程都是可見(jiàn)的。

     public static ThreadLocal<Integer> threadLocalData = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 1;
        }
    };
    
    public static void main(String[] args) {
        System.out.println(threadLocalData.get());//輸出為1
    }
    
    

    實(shí)例化 ThreadLocal 對(duì)象并將其初始值設(shè)置為 1。對(duì)所有線(xiàn)程來(lái)講,它們拿到的初始值都是 1。

  3. 讀取 ThreadLocal 值

    get()方法用于獲取當(dāng)前線(xiàn)程的副本變量值

    public T get()
    
  4. 寫(xiě)入 ThreadLocal 值

    set()方法用于寫(xiě)入當(dāng)前線(xiàn)程的副本變量值

    public void set(T value)
    
  5. 刪除 ThreadLocal 值

    remove()方法移除當(dāng)前前程的副本變量值。每次 remove()之后都會(huì)對(duì)副本變量應(yīng)用一次 initialValue(),恢復(fù)副本的初始值。所以 remove()之后再 get()得到的是初始值。

    public void remove()
    
  6. example

    我們用一個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明這幾個(gè)接口的使用方式。

     //初始化ThreadLocal對(duì)象,并將其初始值設(shè)置為1,在Java8中使用Lambda構(gòu)造方式
    public static ThreadLocal<Integer> threadLocalData = ThreadLocal.withInitial(() -> 1);
    
    public static void main(String[] args) {
    
         //先使用remove()移除值,再獲取值
         new Thread(() -> {
             threadLocalData.remove();
             System.out.println("remove()&get()" + threadLocalData.get());//輸出remove()&get()1
         }).start();
         //直接使用get()獲取值
         new Thread(() -> System.out.println("get()" + threadLocalData.get())).start();//輸出get()1
         //先使用set()設(shè)置值,再獲取
         new Thread(() -> {
             threadLocalData.set(2);
             System.out.println("set()&get()" + threadLocalData.get());//輸出set()&get()2
         }).start();
     }
    

ThreadLocal 使用場(chǎng)景

什么場(chǎng)景適合使用ThreadLocal類(lèi)呢?

ThreadLocal 主要使用在多線(xiàn)程多實(shí)例(并且每個(gè)線(xiàn)程對(duì)應(yīng)一個(gè)實(shí)例狀態(tài))的對(duì)象訪(fǎng)問(wèn),并且不想顯式的為每個(gè)多線(xiàn)程對(duì)象以參數(shù)傳遞的形式來(lái)傳遞這個(gè)共享變量。總結(jié)起來(lái)還是比較繞口,分開(kāi)來(lái)看場(chǎng)景應(yīng)該滿(mǎn)足以下幾點(diǎn):

  1. 有一個(gè)共享變量,這個(gè)共享變量在應(yīng)用的全局域一般來(lái)說(shuō)只有一個(gè),也就是單例的。一般體現(xiàn)為使用static final修飾。
  2. 這個(gè)共享變量是持有狀態(tài)的,也就是說(shuō)這個(gè)共享變量自身有一個(gè)初始值,但是又被多線(xiàn)程訪(fǎng)問(wèn),每個(gè)線(xiàn)程都會(huì)對(duì)這個(gè)共享值進(jìn)行讀取操作,但是又希望每個(gè)線(xiàn)程有自己的獨(dú)立的副本值。
  3. 要滿(mǎn)足前兩個(gè)條件,可以在 Thread 對(duì)象中設(shè)置一個(gè)字段,用來(lái)存儲(chǔ)這個(gè)線(xiàn)程私有的狀態(tài),但是又不會(huì)采取線(xiàn)程類(lèi)成員變量的方式來(lái)實(shí)現(xiàn),那么就使用ThreadLocal類(lèi)來(lái)隱式的持有這個(gè)共享變量的副本。

我們來(lái)舉個(gè)常用的場(chǎng)景來(lái)說(shuō)明下。在 Web 開(kāi)發(fā)中,經(jīng)常需要對(duì)用戶(hù)的請(qǐng)求時(shí)間進(jìn)行格式化,一般服務(wù)端會(huì)生成一個(gè)當(dāng)前時(shí)間對(duì)象,然后將這個(gè)時(shí)間對(duì)象格式化為字符串類(lèi)型記錄到日志中,而格式化方法往往又是一個(gè)工具類(lèi),所有的線(xiàn)程都調(diào)用這個(gè)工具類(lèi)的格式化方法,就像下面這樣。

import javax.annotation.concurrent.NotThreadSafe;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

@NotThreadSafe
public class ErrorDateUtils {

    private static final SimpleDateFormat FMT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String format(Date date) {
        return FMT.format(date);
    }

    public static Date parse(String dateStr) throws ParseException {
        return FMT.parse(dateStr);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                Date now = new Date();
                String format = format(now);
                try {
                    Date parse = parse(format);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();

        }
    }
}
/**
 * 輸出:
 *  ........
 * Exception in thread "Thread-98" java.lang.NumberFormatException: For input string: ""
 *  at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
 *  at java.lang.Long.parseLong(Long.java:601)
 *  ........
 *  at java.text.DateFormat.parse(DateFormat.java:364)
 *  ........
 */


由于SimpleDateFormat類(lèi)本身不是線(xiàn)程安全的,在多線(xiàn)程訪(fǎng)問(wèn)的情況下產(chǎn)生了異常,那么我們可以自然的想到使用同步對(duì)parse()format()方法進(jìn)行處理。

import javax.annotation.concurrent.ThreadSafe;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

@ThreadSafe
public class SafeButSlowDateUtils {

    private static final SimpleDateFormat FMT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static synchronized String format(Date date) {
        return FMT.format(date);
    }

    public static synchronized Date parse(String dateStr) throws ParseException {
        return FMT.parse(dateStr);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                Date now = new Date();
                String format = format(now);
                try {
                    Date parse = parse(format);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();

        }
    }
}



這樣經(jīng)過(guò)同步處理后確實(shí)不會(huì)發(fā)生線(xiàn)程安全問(wèn)題,但是在大規(guī)模并發(fā)訪(fǎng)問(wèn)時(shí)由于同步的存在每個(gè)用戶(hù)發(fā)起的請(qǐng)求都可能存在鎖競(jìng)爭(zhēng)的情況,拖慢系統(tǒng)的處理速度。當(dāng)然我們可以使用棧封閉,在每個(gè)線(xiàn)程中實(shí)例化SimpleDateFormat對(duì)象,這樣就一勞永逸的解決了線(xiàn)程安全問(wèn)題,例如:

public class SafeButNotGraceParseDateDemo {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                SimpleDateFormat formater = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                Date now = new Date();
                String format = formater.format(now);
                try {
                    Date parse = formater.parse(format);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();

        }
    }
}

嗯,看起來(lái)不錯(cuò),但是這樣做就不再需要DateUtils工具類(lèi)了,并且這個(gè)日期格式轉(zhuǎn)換器每次都要初始化,無(wú)法復(fù)用。

現(xiàn)在看看我們遇到的情況:

  1. 我們需要全局有一個(gè)共享的SimpleDateFormat對(duì)象(共享)
  2. 這個(gè)全局共享的SimpleDateFormat對(duì)象是持有狀態(tài)的,也就是格式化的格式字符串yyyy-MM-dd HH:mm:ss,并且會(huì)對(duì)每個(gè)線(xiàn)程的 Date 對(duì)象應(yīng)用這個(gè)格式化模式字符串進(jìn)行格式化。每個(gè)線(xiàn)程都會(huì)對(duì)這個(gè)共享對(duì)象進(jìn)行讀取操作。(共享對(duì)象有狀態(tài),且線(xiàn)程間狀態(tài)可能不一致)
  3. 我們?yōu)榱藵M(mǎn)足復(fù)用性,不希望在每個(gè)線(xiàn)程中實(shí)例化,SimpleDateFormat對(duì)象(不希望每個(gè)線(xiàn)程顯式持有狀態(tài))

滿(mǎn)足了以上的條件,我們就可以認(rèn)為,目前的場(chǎng)景非常適合使用ThreadLocal類(lèi)來(lái)解決問(wèn)題,共享變量SimpleDateFormat對(duì)象不變,只需要使用ThreadLocal來(lái)做線(xiàn)程綁定,這樣每個(gè)線(xiàn)程都持有SimpleDateFormat對(duì)象的副本,每個(gè)線(xiàn)程都持有私有的狀態(tài),是獨(dú)立互不干擾的。


import javax.annotation.concurrent.ThreadSafe;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

@ThreadSafe
public class SafeAndGraceDateUtils {

    private static final ThreadLocal<SimpleDateFormat> FMT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy" +
            "-MM-dd HH:mm:ss"));


    public static String format(Date date) {
        return FMT.get().format(date);
    }

    public static Date parse(String str) throws ParseException {
        return FMT.get().parse(str);
    }

    public static void main(String[] args) throws ParseException {

        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                Date now1 = new Date();
                String format = format(now1);
                try {
                    Date parse = parse(format);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();

        }
    }
}


使用ThreadLocal對(duì)SimpleDateFormat封裝后,每個(gè)線(xiàn)程都有一個(gè)獨(dú)立的SimpleDateFormat副本,狀態(tài)隔離,這樣就不會(huì)出現(xiàn)線(xiàn)程安全問(wèn)題了。

ThreadLocal 在 MyBatis 中的應(yīng)用

ThreadLocal 的多線(xiàn)程隔離數(shù)據(jù)副本的特性非常適合在管理數(shù)據(jù)庫(kù)連接中應(yīng)用。例如在 Mybatis 中SqlSessionManager中就使用了ThreadLocal進(jìn)行 session 管理。

我們知道SqlSessionManager負(fù)責(zé)維護(hù)管理SqlSession,SqlSessionManager本身是線(xiàn)程安全的,但是 DefaultSqlSession 卻并不是線(xiàn)程安全的。如果多個(gè)并發(fā)線(xiàn)程同時(shí)從SqlSessionManager獲取到同一個(gè)SqlSession實(shí)例,由于SqlSession實(shí)例中包含了數(shù)據(jù)庫(kù)操作相關(guān)的狀態(tài)信息,多個(gè)并發(fā)線(xiàn)程同時(shí)使用一個(gè)SqlSession實(shí)例對(duì)數(shù)據(jù)庫(kù)進(jìn)行讀寫(xiě)操作則會(huì)引起數(shù)據(jù)不一致錯(cuò)誤。所以 Mybatis 選擇了使用 ThreadLocal 來(lái)維護(hù) session,對(duì)每個(gè)線(xiàn)程存儲(chǔ)一個(gè) session 副本,這樣進(jìn)行了數(shù)據(jù)的隔離,防止出現(xiàn)線(xiàn)程安全問(wèn)題。

注意注釋標(biāo)明了Note that this class is not Thread-Safe.,DefaultSqlSession本身并不是線(xiàn)程安全的。



/**
 *
 * The default implementation for {@link SqlSession}.
 * Note that this class is not Thread-Safe.
 *
 * @author Clinton Begin
 */
public class DefaultSqlSession implements SqlSession {

  private final Configuration configuration;
  private final Executor executor;

  private final boolean autoCommit;
  private boolean dirty;
  private List<Cursor<?>> cursorList;

  public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
    this.configuration = configuration;
    this.executor = executor;
    this.dirty = false;
    this.autoCommit = autoCommit;
  }

SqlSessionManager使用成員變量localSqlSession來(lái)維護(hù)數(shù)據(jù)庫(kù)會(huì)話(huà)。



/**
 * @author Larry Meadors
 */
public class SqlSessionManager implements SqlSessionFactory, SqlSession {

  private final SqlSessionFactory sqlSessionFactory;
  private final SqlSession sqlSessionProxy;

  private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<SqlSession>();

 // ......

    @Override
  public Connection getConnection() {
    final SqlSession sqlSession = localSqlSession.get();
    if (sqlSession == null) {
      throw new SqlSessionException("Error:  Cannot get connection.  No managed session is started.");
    }
    return sqlSession.getConnection();
  }

  @Override
  public void clearCache() {
    final SqlSession sqlSession = localSqlSession.get();
    if (sqlSession == null) {
      throw new SqlSessionException("Error:  Cannot clear the cache.  No managed session is started.");
    }
    sqlSession.clearCache();
  }

  @Override
  public void commit() {
    final SqlSession sqlSession = localSqlSession.get();
    if (sqlSession == null) {
      throw new SqlSessionException("Error:  Cannot commit.  No managed session is started.");
    }
    sqlSession.commit();
  }



在獲取連接getConnection()等方法中,即使多線(xiàn)程訪(fǎng)問(wèn),也是使用localSqlSession.get()來(lái)獲取線(xiàn)程本地綁定的localSqlSession對(duì)象副本。

ThreadLocal 使用總結(jié)

概括起來(lái)說(shuō),對(duì)于多線(xiàn)程資源共享的問(wèn)題,同步機(jī)制采用了“以時(shí)間換空間”的方式,而 ThreadLocal另辟蹊徑采用了“以空間換時(shí)間”的方式來(lái)實(shí)現(xiàn)了數(shù)據(jù)的隔離。
前者僅提供一份變量,讓不同的線(xiàn)程排隊(duì)訪(fǎng)問(wèn),而后者為每一個(gè)線(xiàn)程都提供了一份變量,因此可以同時(shí)訪(fǎng)問(wèn)而互不影響。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容