溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Java 并發(fā)編程之ThreadLocal詳解及實(shí)例

發(fā)布時(shí)間:2020-09-12 05:11:56 來源:腳本之家 閱讀:163 作者:書呆子Rico 欄目:編程語言

Java 理解 ThreadLocal

摘要:

  ThreadLocal 又名線程局部變量,是 Java 中一種較為特殊的線程綁定機(jī)制,用于保證變量在不同線程間的隔離性,以方便每個(gè)線程處理自己的狀態(tài)。進(jìn)一步地,本文以ThreadLocal類的源碼為切入點(diǎn),深入分析了ThreadLocal類的作用原理,并給出應(yīng)用場景和一般使用步驟。

一. 對 ThreadLocal 的理解

1). ThreadLocal 概述

  ThreadLocal 又名 線程局部變量,是 Java 中一種較為特殊的 線程綁定機(jī)制,可以為每一個(gè)使用該變量的線程都提供一個(gè)變量值的副本,并且每一個(gè)線程都可以獨(dú)立地改變自己的副本,而不會(huì)與其它線程的副本發(fā)生沖突。通過 ThreadLocal 存取的數(shù)據(jù),總是與當(dāng)前線程相關(guān),也就是說,JVM 為每個(gè)運(yùn)行的線程綁定了私有的本地實(shí)例存取空間,從而為多線程環(huán)境常出現(xiàn)的并發(fā)訪問問題提供了一種 隔離機(jī)制 。

2). ThreadLocal 在 JDK 中的定義

ThreadLocal

This class provides thread-local variables. These variables differ from their normal counterparts(副本) in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
Each thread holds an implicit reference to its copy of a thread-local variable (見下圖)
as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

       Java 并發(fā)編程之ThreadLocal詳解及實(shí)例

我們可以從中摘出三條要點(diǎn):

每個(gè)線程都有關(guān)于該 ThreadLocal變量 的私有值

 每個(gè)線程都有一個(gè)獨(dú)立于其他線程的上下文來保存這個(gè)變量的值,并且對其他線程是不可見的。

獨(dú)立于變量的初始值

 ThreadLocal 可以給定一個(gè)初始值,這樣每個(gè)線程就會(huì)獲得這個(gè)初始化值的一個(gè)拷貝,并且每個(gè)線程對這個(gè)值的修改對其他線程是不可見的。

狀態(tài)與某一個(gè)線程相關(guān)聯(lián)

 ThreadLocal 不是用于解決共享變量的問題的,也不是為了協(xié)調(diào)線程同步而存在,而是為了方便每個(gè)線程處理自己的狀態(tài)而引入的一個(gè)機(jī)制,理解這點(diǎn)對正確使用 ThreadLocal 至關(guān)重要。

3). 應(yīng)用場景

  類 ThreadLocal 主要解決的就是為每個(gè)線程綁定自己的值,以方便其處理自己的狀態(tài)。形象地講,可以將 ThreadLocal變量 比喻成全局存放數(shù)據(jù)的盒子,盒子中可以存儲(chǔ)每個(gè)線程的私有數(shù)據(jù)。例如,以下類用于生成對每個(gè)線程唯一的局部標(biāo)識符。線程 ID 是在第一次調(diào)用 uniqueNum.get() 時(shí)分配的,在后續(xù)調(diào)用中不會(huì)更改。

import java.util.concurrent.atomic.AtomicInteger;

public class UniqueThreadIdGenerator {
  private static final AtomicInteger uniqueId = new AtomicInteger(0);

  private static final ThreadLocal<Integer> uniqueNum = new ThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
      return uniqueId.getAndIncrement();
    }
  };

  public static void main(String[] args) {
    Thread[] threads = new Thread[5];
    for (int i = 0; i < 5; i++) {
      String name = "Thread-" + i;
      threads[i] = new Thread(name){
        @Override
        public void run() {
          System.out.println(Thread.currentThread().getName() + ": "
              + uniqueNum.get());
        }
      };
      threads[i].start();
    }

    System.out.println(Thread.currentThread().getName() + ": "
        + uniqueNum.get());
  }
}/* Output(輸出結(jié)果不唯一): 
    Thread-1: 2
    Thread-0: 0
    Thread-2: 3
    main: 1
    Thread-3: 4
    Thread-4: 5
 *///:~

二. 深入分析ThreadLocal類

  下面,我們來看一下 ThreadLocal 的具體實(shí)現(xiàn),該類一共提供的四個(gè)方法:

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }

  其中,get()方法是用來獲取 ThreadLocal變量 在當(dāng)前線程中保存的值,set() 用來設(shè)置 ThreadLocal變量 在當(dāng)前線程中的值,remove() 用來移除當(dāng)前線程中相關(guān) ThreadLocal變量,initialValue() 是一個(gè) protected 方法,一般需要重寫。

1、 原理探究

1). 切入點(diǎn):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();  // 獲取當(dāng)前線程對象
    ThreadLocalMap map = getMap(t);   // 獲取當(dāng)前線程的成員變量 threadLocals
    if (map != null) {
      // 從當(dāng)前線程的 ThreadLocalMap 獲取該 thread-local variable 對應(yīng)的 entry
      ThreadLocalMap.Entry e = map.getEntry(this);  
      if (e != null)   
        return (T)e.value;  // 取得目標(biāo)值
    }
    return setInitialValue(); 
  }

2).關(guān)鍵點(diǎn):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();   // 默認(rèn)實(shí)現(xiàn)返回 null
    Thread t = Thread.currentThread();  // 獲得當(dāng)前線程
    ThreadLocalMap map = getMap(t);   // 得到當(dāng)前線程 ThreadLocalMap類型域 threadLocals
    if (map != null)
      map.set(this, value); // 該 map 的鍵是當(dāng)前 ThreadLocal 對象
    else
      createMap(t, value);  
    return value;
  }

  我們緊接著看上述方法涉及到的三個(gè)方法:initialValue(),set(this, value) 和 createMap(t, value)。

(1) initialValue()

 /**
   * 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 <tt>initialValue</tt> 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 <tt>null</tt>; if the
   * programmer desires thread-local variables to have an initial
   * value other than <tt>null</tt>, <tt>ThreadLocal</tt> 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;      // 默認(rèn)實(shí)現(xiàn)返回 null
  }

(2) createMap()

/**
   * 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); // this 指代當(dāng)前 ThreadLocal 變量,為 map 的鍵 
  }

至此,可能大部分朋友已經(jīng)明白了 ThreadLocal類 是如何為每個(gè)線程創(chuàng)建變量的副本的:

  ① 在每個(gè)線程Thread內(nèi)部有一個(gè) ThreadLocal.ThreadLocalMap 類型的成員變量 threadLocals,這個(gè)threadLocals就是用來存儲(chǔ)實(shí)際的ThreadLocal變量副本的,鍵值為當(dāng)前ThreadLocal變量,value為變量的副本(值);

 ?、?初始時(shí),在Thread里面,threadLocals為空,當(dāng)通過ThreadLocal變量調(diào)用get()方法或者set()方法,就會(huì)對Thread類中的threadLocals進(jìn)行初始化,并且以當(dāng)前ThreadLocal變量為鍵值,以ThreadLocal要保存的值為value,存到 threadLocals;

 ?、?然后在當(dāng)前線程里面,如果要使用副本變量,就可以通過get方法在對應(yīng)線程的threadLocals里面查找。

2、實(shí)例驗(yàn)證

  下面通過一個(gè)例子來證明通過ThreadLocal能達(dá)到在每個(gè)線程中創(chuàng)建變量副本的效果:

public class Test {

  ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
  ThreadLocal<String> stringLocal = new ThreadLocal<String>();

  public void set() {
    longLocal.set(Thread.currentThread().getId());
    stringLocal.set(Thread.currentThread().getName());
  }

  public long getLong() {
    return longLocal.get();
  }

  public String getString() {
    return stringLocal.get();
  }

  public static void main(String[] args) throws InterruptedException {
    final Test test = new Test();

    test.set();
    System.out.println("父線程 main :");
    System.out.println(test.getLong());
    System.out.println(test.getString());

    Thread thread1 = new Thread() {
      public void run() {
        test.set();
        System.out.println("\n子線程 Thread-0 :");
        System.out.println(test.getLong());
        System.out.println(test.getString());
      };
    };
    thread1.start();
  }
}/* Output: 
    父線程 main :
          1
          main

    子線程 Thread-0 :
          12
          Thread-0
 *///:~

  從這段代碼的輸出結(jié)果可以看出,在main線程中和thread1線程中,longLocal保存的副本值和stringLocal保存的副本值都不一樣,并且進(jìn)一步得出:

  • 實(shí)際上,通過 ThreadLocal 創(chuàng)建的副本是存儲(chǔ)在每個(gè)線程自己的threadLocals中的;
  • 為何 threadLocals 的類型 ThreadLocalMap 的鍵值為 ThreadLocal 對象,因?yàn)槊總€(gè)線程中可有多個(gè) threadLocal變量,就像上面代碼中的 longLocal 和 stringLocal;
  • 在進(jìn)行g(shù)et之前,必須先set,否則會(huì)報(bào)空指針異常;若想在get之前不需要調(diào)用set就能正常訪問的話,必須重寫initialValue()方法。

三. ThreadLocal的應(yīng)用場景

  在 Java 中,類 ThreadLocal 解決的是變量在不同線程間的隔離性。 最常見的 ThreadLocal 使用場景有 數(shù)據(jù)庫連接問題、Session管理等。

(1) 數(shù)據(jù)庫連接問題

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
  public Connection initialValue() {
    return DriverManager.getConnection(DB_URL);
  }
};

public static Connection getConnection() {
  return connectionHolder.get();
}

(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 一般使用步驟

ThreadLocal 使用步驟一般分為三步:

  • 創(chuàng)建一個(gè) ThreadLocal 對象 threadXxx,用來保存線程間需要隔離處理的對象 xxx;
  • 提供一個(gè)獲取要隔離訪問的數(shù)據(jù)的方法 getXxx(),在方法中判斷,若 ThreadLocal對象為null時(shí)候,應(yīng)該 new() 一個(gè)隔離訪問類型的對象;
  • 在線程類的run()方法中,通過getXxx()方法獲取要操作的數(shù)據(jù),這樣可以保證每個(gè)線程對應(yīng)一個(gè)數(shù)據(jù)對象,在任何時(shí)刻都操作的是這個(gè)對象,不會(huì)交叉。

感謝閱讀,希望能幫助到大家,謝謝大家對本站的支持!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI