Thread詳解11:ThreadLocal的使用

首先,我們看看JDK文檔是怎麼描述這個類的:

  • 該類提供了線程局部 (thread-local) 變量。這些變量不同於它們的普通對應物,因爲訪問某個變量(通過其 get 或 set 方法)的每個線程都有自己的局部變量,它獨立於變量的初始化副本。ThreadLocal 實例通常是類中的 private static 字段,它們希望將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。每個線程都保持對其線程局部變量副本的隱式引用,只要線程是活動的並且 ThreadLocal 實例是可訪問的;在線程消失之後,其線程局部實例的所有副本都會被垃圾回收(除非存在對這些副本的其他引用)。

好吧,看完之後我還是不知道它是幹什麼的。爲了講解的需要,我先直接給出 ThreadLocal 的概述,再一點點分析證明。

ThreadLocal主要解決的問題是給每個線程綁定自己的值,這個值是和線程綁定的,是線程的局部變量,是其他線程沒法訪問的。所以,博主的【Thread詳解系列1-10】講的基本上都是併發訪問的問題,這一篇11卻不是講併發。我們可以把 ThreadLocal 類比喻成全局存放數據的盒子,這個盒子中每個線程往裏面放數據,那這個數據就是它私有的,程序員也就不用自己去維護這個對應關係。這就是 ThreadLocal 的價值所在。

先來看看 ThreadLocal 的API,再結合我的示例代碼去了解它做了什麼,後面我再證明它的價值。


1 用法示例

  • T get() :返回此線程局部變量的當前線程副本中的值。
  • protected T initialValue() :返回此線程局部變量的當前線程的“初始值”。
  • void remove():移除此線程局部變量當前線程的值。
  • void set(T value) :將此線程局部變量的當前線程副本中的值設置爲指定值。

UniqueThreadIdGenerator.java

package threadLocalTest;

import java.util.concurrent.atomic.AtomicInteger;

public class UniqueThreadIdGenerator {

    private static AtomicInteger uniqueId = new AtomicInteger(0);

    private static ThreadLocal<Integer> uniqueNum = new ThreadLocal<Integer>() {
        @Override
        // 如果當前線程是第一次請求id的分配則給它賦一個初始值
        protected Integer initialValue() {
            return uniqueId.getAndIncrement();
        }
    };

    // 給當前線程返回它的id
    public static int getCurrentThreadId() {
        return uniqueNum.get();
    }

    // 設置當前線程的id
    public static void setCurrentThreadId(int id) {
        uniqueNum.set(id);
    }

}

Thread1.java

package threadLocalTest;

public class Thread1 implements Runnable {

    @Override
    public void run() {
        // 線程的id是在它第一次run的時候才分配的,它run,它請求分配id,系統給它一個id
        int id = UniqueThreadIdGenerator.getCurrentThreadId();
        System.out.println(Thread.currentThread().getName() + " is running, its ID is: " + id);

        // 三次向系統請求數據
        for (int i = 0; i < 3; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " is asking for data, my ID is:" + id);
        }
        System.out.println(Thread.currentThread().getName() + " is over!----------");
    }

    public static void main(String[] args) {
        // 新建3個線程
        Thread tA = new Thread(new Thread1(), "A");
        Thread tB = new Thread(new Thread1(), "B");
        Thread tC = new Thread(new Thread1(), "C");
        tA.start();
        tB.start();
        tC.start();
    }
}

輸出

第一次運行:

B is running, its ID is: 0
A is running, its ID is: 2
C is running, its ID is: 1
A is asking for data, my ID is:2
B is asking for data, my ID is:0
C is asking for data, my ID is:1
C is asking for data, my ID is:1
B is asking for data, my ID is:0
A is asking for data, my ID is:2
B is asking for data, my ID is:0
C is asking for data, my ID is:1
C is over!----------
A is asking for data, my ID is:2
B is over!----------
A is over!----------

第N次運行:

C is running, its ID is: 0
A is running, its ID is: 1
B is running, its ID is: 2
C is asking for data, my ID is:0
A is asking for data, my ID is:1
B is asking for data, my ID is:2
A is asking for data, my ID is:1
C is asking for data, my ID is:0
B is asking for data, my ID is:2
A is asking for data, my ID is:1
C is asking for data, my ID is:0
A is over!----------
C is over!----------
B is asking for data, my ID is:2
B is over!----------

上面的例子我提醒大家注意一點:

  • Thread1類沒有一個 private id 這樣一個成員變量,從而也沒有在構造方法中用一個參數傳入一個id,它的id是在run的時候才由外部邏輯生成的,而且不需要程序員主動去維護。


2 典型應用

最典型的應用就是在連接數據庫的時候,線程與數據庫連接的session是由數據庫/服務器分配的,不是線程本身維護的,但是線程在編寫一些對數據庫的操作的時候卻要用到session。那怎麼辦呢?

一種常規的做法可能是每次線程要連接數據庫的時候就向數據庫請求session,然後以參數的形式傳入線程。而且對於session的分配還要做好併發處理,你不能給兩個線程分配了同一個session,這樣會造成很多混亂。而使用ThreadLocal就不用考慮這麼多,減輕了程序員的負擔。

    public class HibernateUtil {
        private static Log log = LogFactory.getLog(HibernateUtil.class);
        private static final SessionFactory sessionFactory;     //定義SessionFactory

        static {
            try {
                // 通過默認配置文件hibernate.cfg.xml創建SessionFactory
                sessionFactory = new Configuration().configure().buildSessionFactory();
            } catch (Throwable ex) {
                log.error("初始化SessionFactory失敗!", ex);
                throw new ExceptionInInitializerError(ex);
            }
        }

        //創建線程局部變量session,用來保存Hibernate的Session
        public static final ThreadLocal session = new ThreadLocal();

        /**
         * 獲取當前線程中的Session
         * @return Session
         * @throws HibernateException
         */
        public static Session currentSession() throws HibernateException {
            Session s = (Session) session.get();
            // 如果Session還沒有打開,則新開一個Session
            if (s == null) {
                s = sessionFactory.openSession();
                session.set(s);         //將新開的Session保存到線程局部變量中
            }
            return s;
        }

        public static void closeSession() throws HibernateException {
            //獲取線程局部變量,並強制轉換爲Session類型
            Session s = (Session) session.get();
            session.set(null);
            if (s != null)
                s.close();
        }
    }


3 get 與 null

如果線程是第一次調用ThreadLocal的get方法,請求一個數據,那麼get 返回的值一定是null。這個時候有兩種等效的處理方式。

  1. 自己顯示地增加一個邏輯:如果get 返回null,則new 一個對象,再調用set 方法把這個值放到ThreadLocal裏面去。
  2. 像section1中的示例代碼一樣, override ThreadLocal 的 initialValue 方法
發佈了66 篇原創文章 · 獲贊 15 · 訪問量 24萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章