Java多線程 | 03 線程本地ThreadLocal的介紹與使用

目錄

 

第03課 線程本地 ThreadLocal 的介紹與使用

ThreadLocal 概述

ThreadLocal 與 Synchronized 同步機制的比較

如何實現一個簡單的 ThreadLocal

ThreadLocal 的應用


第03課 線程本地 ThreadLocal 的介紹與使用

ThreadLocal 概述

我們通過上兩篇的學習,我們已經知道了變量值的共享可以使用public static變量的形式,所有的線程都使用同一個被public static修飾的變量。

那麼如果我們想實現每一個線程都有自己的共享變量該如何解決呢?JDK 提供的 ThreadLocal 正是爲了解決這樣的問題的。

ThreadLocal 主要解決的就是每個線程綁定自己的值,可以將 ThreadLocal 類比喻成全局存放數據的盒子,盒子中可以存儲每個線程的私有變量。

先舉個例子:

public class ThreadLocalDemo {

    public static ThreadLocal<List<String>> threadLocal = new ThreadLocal<>();

    public void setThreadLocal(List<String> values) {
        threadLocal.set(values);
    }

    public void getThreadLocal() {
        System.out.println(Thread.currentThread().getName());
        threadLocal.get().forEach(name -> System.out.println(name));
    }

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

        final ThreadLocalDemo threadLocal = new ThreadLocalDemo();
        new Thread(() -> {
            List<String> params = new ArrayList<>(3);
            params.add("張三");
            params.add("李四");
            params.add("王五");
            threadLocal.setThreadLocal(params);
            threadLocal.getThreadLocal();
        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(1000);
                List<String> params = new ArrayList<>(2);
                params.add("Chinese");
                params.add("English");
                threadLocal.setThreadLocal(params);
                threadLocal.getThreadLocal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

運行結果:

Thread-0
張三
李四
王五
Thread-1
Chinese
English

可以,看出雖然多個線程對同一個變量進行訪問,但是由於threadLocal變量由ThreadLocal 修飾,則不同的線程訪問的就是該線程設置的值,這裏也就體現出來ThreadLocal的作用。

當使用ThreadLocal維護變量時,ThreadLocal爲每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。

ThreadLocal 與 Synchronized 同步機制的比較

在同步機制中,通過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序慎密地分析什麼時候對變量進行讀寫,什麼時候需要鎖定某個對象,什麼時候釋放對象鎖等繁雜的問題,程序設計和編寫難度相對較大。

ThreadLocal 是線程局部變量,是一種多線程間併發訪問變量的解決方案。和 Synchronized 等加鎖的方式不同,ThreadLocal 完全不提供鎖,而使用以空間換時間的方式,爲每個線程提供變量的獨立副本,以保證線程的安全。

如何實現一個簡單的 ThreadLocal

public class SimpleThreadLocal<T> {

    /**
     * Key爲線程對象,Value爲傳入的值對象
     */
    private static Map<Thread, T> valueMap = Collections.synchronizedMap(new HashMap<Thread, T>());

    /**
     * 設值
     * @param value Map鍵值對的value
     */
    public void set(T value) {
        valueMap.put(Thread.currentThread(), value);
    }

    /**
     * 取值
     * @return
     */
    public T get() {
        Thread currentThread = Thread.currentThread();
        //返回當前線程對應的變量
        T t = valueMap.get(currentThread);
        //如果當前線程在Map中不存在,則將當前線程存儲到Map中
        if (t == null && !valueMap.containsKey(currentThread)) {
            t = initialValue();
            valueMap.put(currentThread, t);
        }
        return t;
    }

    public void remove() {
        valueMap.remove(Thread.currentThread());
    }

    public T initialValue() {
        return null;
    }

    public static void main(String[] args) {

        SimpleThreadLocal<List<String>> threadLocal = new SimpleThreadLocal<>();

        new Thread(() -> {
            List<String> params = new ArrayList<>(3);
            params.add("張三");
            params.add("李四");
            params.add("王五");
            threadLocal.set(params);
            System.out.println(Thread.currentThread().getName());
            threadLocal.get().forEach(param -> System.out.println(param));
        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(1000);
                List<String> params = new ArrayList<>(2);
                params.add("Chinese");
                params.add("English");
                threadLocal.set(params);
                System.out.println(Thread.currentThread().getName());
                threadLocal.get().forEach(param -> System.out.println(param));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
} 

運行結果:

這裏寫圖片描述

雖然上面的代碼清單中的這個 ThreadLocal 實現版本顯得比較簡單粗糙,但其目的主要在於呈現 JDK 中所提供的 ThreadLocal 類在實現上的思路。

關於如何設計 ThreadLocal 的思路以及其原理會在後文中詳細介紹,這裏只做一個簡單的預熱。

ThreadLocal 的應用

MyBatis 的使用

enter image description here

SqlSessionManager 類部分代碼如下:

private 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 commit() {
    final SqlSession sqlSession = localSqlSession.get();
    if (sqlSession == null) {
        throw new SqlSessionException("Error:  Cannot commit.  No managed session is started.");
    }
    sqlSession.commit();
}

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

enter image description here

從上圖可能看出,在 MyBatis 中,SqlSessionManager 類不但實現了 SqlSession 接口,同時也實現了 SqlSessionFactory 接口。而我們平時使用到的最多的就是 DefaultSqlSession,實現了 SqlSession,SqlSession 接口的實現如下:

enter image description here

SqlSessionManager 的作用如下:

enter image description here

  1. SqlSessionFactoryBuilder 負責接收 mybatis-config.xml 的輸入流,創建 DefaultSqlSessionFactory 實例。

  2. DefaultSqlSessionFactory 實現了 SqlSessionFactory 接口。

  3. SqlSessionManager 實現了 SqlSessionFactory 接口,又封裝了 DefaultSqlSessionFactory。

拿出 SqlSessionManager 的一個方法 getConnection 解釋一下:

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

可以看出 localSqlSession 是一個 ThreadLocal 變量,是每一個線程私有的,當有一個線程請求獲取 Connection 的時候,會首先獲取當前線程 ThreadLocal 中的 SqlSession,然後由 SqlSession 獲取 Connection 對象,一個 ThreadLocal 的簡單使用。

數據庫主從複製時讀寫分離的 ThreadLocal 使用

其實,在 MyBatis 中對 ThreadLocal 的使用主要體現在數據庫連接這塊,我們不僅聯想到我們在實現主從複製讀寫分離的時候,我們是否也是用到了 ThreadLocal,先看示例:

enter image description here

上述簡單的實現了數據源的 Handler 類 DataSourceHandler,在下邊的類中會實現讀寫數據庫的切換:

enter image description here

enter image description here

enter image description here

根據 AOP 切面編程獲取方法類型,根據方法的類型判斷是讀庫還是寫庫,如果是讀庫的話就爲當前線程設置訪問讀庫的數據庫信息,詳細數據庫主從複製讀寫分離的 AOP 實現案例,可以參考代碼:

https://gitee.com/xuliugen/aop-choose-db-demo

enter image description here

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