頭條面試官手把手教學 ThreadLocal

SoWhat:麥叔,最近面別的公司沒? ios

麥叔:上次面試失敗桑心死我了,我沉澱了一禮拜面頭條去了。web

SoWhat:哎呦我去!麥叔你這頭條都面上了,面了幾輪,手寫紅黑樹沒? 面試

麥叔:剛剛兩輪,一面紅黑樹輕鬆搞定了!面我關於Java的JVM跟併發的時候我看你水的那個JVM系列還有併發系列都過了。最後還問了我點ThreadLocal的問題。 sql

SoWhat:擦,ThreadLocal有啥好問的就是個底層Map啊!而且平常我寫數據庫事務跟Spring的時候也沒見用啊!問那麼偏門的幹什麼他們。數據庫

麥叔:擦。。。。你關於ThreadLocal知道的那麼點啊?Spring的靈魂除了IOC跟AOP就是ThreadLocal了!數組

SoWhat:真的麼,麥叔你給我講講要不? 麥叔:好今天讓你開開眼。微信

在這裏插入圖片描述

介紹

咱們看下JDK文檔的官方描述:ThreadLocal類用來提供線程內部等局部變量,這種變量在多線程環境下訪問(get,set)時能保證各個線程的變量相對獨立於其餘線程內的變量,ThreadLocal實例一般來講都是private static類型,用於關聯線程的上下文。 ThreadLocal做用:提供線程內部的局部變量,不一樣線程之間不會被相互干擾,該變量在線程生命週期內起做用,能夠減小同一個線程內多個函數或者組件之間一些公共變量傳遞的複雜度。多線程

  1. 線程併發:在多線程併發環境下用
  2. 傳遞數據:經過ThrealLocal在同一個線程,不一樣組件中傳遞公共變量。
  3. 線程隔離:每一個線程內變量是獨立的,不會相互影響。

初探使用

使用的時候能夠簡單的理解爲ThreadLocal維護這一個HashMap,其中key = 當前線程,value = 當前線程綁定的局部變量。併發

方法 用途
ThreadLocal 建立ThreadLocal對象
set(T value) 設置當前線程綁定的局部變量
T get() 得到當前線程綁定的局部變量
remove() 移除當前線程綁定的局部變量

ThreadLocal使用編輯器

  1. 先是不用
public class UserThreadLocal {
    private String str = "";
    public String getStr() {return str;}
    public void setStr(String j) {this.str = j;}
    public static void main(String[] args) {
        UserThreadLocal userThreadLocal = new UserThreadLocal();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    userThreadLocal.setStr(Thread.currentThread().getName() + "的數據");
                    System.out.println(Thread.currentThread().getName() + " 編號 " + userThreadLocal.getStr());
                }
            });
            thread.setName("線程" + i);
            thread.start();
        }
    }
}

重複執行幾回會出現以下結果:2. 用Synchronized

  synchronized (UserThreadLocal.class
  // 惟一區別就是用了同步方法塊
   userThreadLocal.setStr(Thread.currentThread().getName() + "的數據");
 System.out.println(Thread.currentThread().getName() + " 編號 " + userThreadLocal.getStr());
  }
 }

多執行幾回結果總能正確:3. 用了ThreadLocal

public class UserThreadLocal {
    static ThreadLocal<String> str = new ThreadLocal<>();
    public String getStr() {return str.get();}
    public void setStr(String j) {str.set(j);}
    public static void main(String[] args) {
        UserThreadLocal userThreadLocal = new UserThreadLocal();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    userThreadLocal.setStr(Thread.currentThread().getName() + "的數據");
                    System.out.println(Thread.currentThread().getName() + " 編號 " + userThreadLocal.getStr());
                }
            });
            thread.setName("線程" + i);
            thread.start();
        }
    }
}

重複執行結果以下:結論: 多個線程同時對同一個共享變量裏對一些屬性賦值會產生不一樣步跟數據混亂,加鎖經過如今同步使用能夠實現有效性,經過ThreadLocal也能夠實現。

對比 synchronized ThreadLocal
原理 以時間換正確性,不一樣線程排隊訪問 以空間換取準確性,爲每個線程都提供了一份變量副本,從而實現訪問互不干擾
側重點 多個線程之間訪問資源對同步 多線程中讓每一個線程之間的數據相互隔離

再度使用

數據庫轉帳系統,必定要確保轉出轉入具有事務性,JDBC中關於事務的API。

Connection接口方法 做用
setAutoCommit(false) 禁止事務自動提交,默認是自動的
commit() 提交事務
rollback() 回滾事務

代碼實現

分析轉帳業務,咱們先將業務分4層。

  1. dao層:鏈接數據庫進行數據庫的crud。
public class AccountDao {
    public void out(String outUser, int money) throws SQLException {
        String sql = "update account set money = money - ?  where name = ?";
        Connection conn = JdbcUtils.getConnection();// 數據庫鏈接池獲取鏈接
        PreparedStatement preparedStatement = conn.prepareStatement(sql);
        preparedStatement.setInt(1, money);
        preparedStatement.setString(2, outUser);
        preparedStatement.executeUpdate();
        JdbcUtils.release(preparedStatement, conn);
    }

    public void in(String inUser, int money) throws SQLException {
        String sql = "update account set money = money + ?  where name = ?";
        Connection conn = JdbcUtils.getConnection();//數據庫鏈接池得到鏈接
        PreparedStatement preparedStatement = conn.prepareStatement(sql);
        preparedStatement.setInt(1, money);
        preparedStatement.setString(2, inUser);
        preparedStatement.executeUpdate();
        JdbcUtils.release(preparedStatement, conn);
    }
}
  1. service層:開啓跟關閉事務,調用dao層。
public class AccountService {
    public boolean transfer(String outUser, String inUser, int money) {
        AccountDao ad = new AccountDao(); // service 調用dao層
        Connection conn = null;
        try {
            // 開啓事務
            conn = JdbcUtils.getConnection();// 數據庫鏈接池得到鏈接
            conn.setAutoCommit(false);// 關閉自動提交

            ad.out(outUser, money);//轉出
            int i = 1/0;// 此時故意用一個異常來檢查數據庫的事務性。
            ad.in(inUser, money);//轉入
            // 上面這兩個要有原子性
            JdbcUtils.commitAndClose(conn);//成功提交
        } catch (SQLException e) {
            e.printStackTrace();
            JdbcUtils.rollbackAndClose(conn);//失敗回滾
            return false;
        }
        return true;
    }
}
  1. utils層:數據庫鏈接池的關閉跟獲取。
public class JdbcUtils {
    private static final ComboBoxPopupControl ds = new ComboPooledDataSource();
    public static Connection getConnection() throws SQLException {
        return ds.getConnection();// 從數據庫鏈接池得到一個鏈接
    }
    public static void release(AutoCloseable... ios) {
        for (AutoCloseable io : ios) {
            if (io != null) {
                try {
                    io.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void commitAndClose(Connection conn) {
        try {// 提交跟關閉
            if (conn != null) {
                conn.commit();
                conn.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public  static void rollbackAndClose(Connection conn){
        try{//回滾跟關閉
            if(conn!=null){
                conn.rollback();
                conn.close();
            }
        }catch (SQLException e){
            e.printStackTrace();
        }
    }
}
  1. web層: 真正的調用入口。

public class AccountWeb {
    public static void main(String[] args) {
        String outUser = "SoWhat";
        String inUser = "小麥";
        int money = 100;
        AccountService as = new AccountService();
        boolean result =  as.transfer(outUser,inUser,money);
        if(result == false){
            System.out.println("轉帳失敗");
        }
        else{
            System.out.println("轉帳成功");
        }
    }
}

注意點

  1. 爲了保證因此操做在一個事務中,案例中鏈接必須是同一個, service層開啓事務的 connection須要跟 dao層訪問數據庫的 connection 保持一致
  2. 線程併發的狀況下,每一個線程只能操做各自的 connection。 上述注意點在代碼中的體現爲service層獲取鏈接開啓事務的要跟dao層的鏈接一致,而且在當前線程只能操做本身的鏈接。

尋常思路

  1. 傳參:將service層connection對象直接傳遞到dao層,
  2. 加鎖 常規代碼更改以下:

弊端:

  1. 提升代碼耦合度:service層connection對象傳遞到dao層了。
  2. 下降了程序到性能:由於加鎖下降了系統性能。
  3. Spring採用 Threadlocal的方式,來保證單個線程中的數據庫操做使用的是同一個數據庫鏈接,同時,採用這種方式可使業務層使用事務時不須要感知並管理connection對象,經過傳播級別,巧妙地管理多個事務配置之間的切換,掛起和恢復。

ThreadLocal思路

ThreadLocal來實現,核心思想就是servicedao從數據庫鏈接確保用到同一個。utils修改部分代碼以下:

    static ThreadLocal<Connection> tl = new ThreadLocal<>();

    private static final ComboBoxPopupControl ds = new ComboPooledDataSource();

    public static Connection getConnection() throws SQLException {
        Connection conn = tl.get();
        if (conn == null) {
            conn = ds.getConnection();
            tl.set(conn);
        }
        return conn;
    }

    public static void commitAndClose(Connection conn) {
        try {
            if (conn != null) {
                conn.commit();
                tl.remove(); //相似IO流操做 用完釋放 避免內存泄漏 詳情看下面分析
                conn.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

ThreadLocal優點:

1.數據傳遞:保存每一個線程綁定的數據,在須要的地方直接獲取,避免參數傳遞帶來的耦合性。 2. 線程隔離:各個線程之間的數據相互隔離又具備併發性,避免同步加鎖帶來的性能損失。

底層

誤解

不看源碼僅僅從咱們使用跟別人告訴咱們的角度去考慮咱們會認爲ThreadLocal設計的思路:一個共享的Map,其中每個子線程=Key,該子線程對應存儲的ThreadLocal值=Value。JDK早期確實是以下這樣設計的,不過如今早已不是!

JDK8中設計

在JDK8中ThreadLocal的設計是:每個Thread維護一個Map,這個MapkeyThreadLocal對象,value纔是真正要存儲的object,過程以下:

  1. 每個Thread線程內部都有一個Map(ThreadLocalMap),一個線程能夠有多個TreadLocal來存放不一樣類型的對象的,可是他們都將放到你當前線程的ThreadLocalMap裏,因此確定要數組來存。
  2. Map裏存儲ThreadLocal對象爲key,線程的變量副本爲value。
  3. Thread內部的Map是由ThreadLocal類維護的,由ThreadLocal負責向map獲取跟設置線程變量值。
  4. 不一樣線程每次獲取副本值時,別的線程沒法得到當前線程的副本值,造成副本隔離,互不干擾。
在這裏插入圖片描述

優點

JDK8設計比JDK早期設計的優點,咱們能夠看到早期跟如今主要的變化就是ThreadThreadLocal調換了位置。

老版:ThreadLocal維護着一個ThreadLocalMap,由Thread來當作這個map裏的key。 新版:Thread維護這一個ThreadLocalMap,由當前的ThreadLocal做爲key。

  1. 每一個Map存儲的KV數據變小了,之前是線程個數多則 ThreadLocal存儲的KV數就變多。如今的K是用 ThreadLocal實例化對象來當key的,多線程狀況下 ThreadLocal實例化個數通常都比線程數少!
  2. 之前線程銷燬後 ThreadLocal這個Map仍是存在的,如今當Thread銷燬時候, ThreadLocalMap也會隨之銷燬,減小內存使用。

ThreadLocal核心方法

ThreadLocal對外暴露的方法有4個:

方法 用途
initialValue() 返回當前線程局部變量初始化值
set(T value) 設置當前線程綁定的局部變量
T get() 得到當前線程綁定的局部變量
remove() 移除當前線程綁定的局部變量
set方法:
// 設置當前線程對應的ThreadLocal值
public void set(T value) {
    Thread t = Thread.currentThread(); // 獲取當前線程對象
    ThreadLocalMap map = getMap(t);
    if (map != null// 判斷map是否存在
        map.set(this, value); 
        // 調用map.set 將當前value賦值給當前threadLocal。
    else
        createMap(t, value);
        // 若是當前對象沒有ThreadLocalMap 對象。
        // 建立一個對象 賦值給當前線程
}

// 獲取當前線程對象維護的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
// 給傳入的線程 配置一個threadlocals
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

執行流程:

  1. 得到當前線程,根據當前線程得到map。
  2. map不爲空則將參數設置到map中,當前到Threadlocal做爲key。
  3. 若是map爲空,給該線程建立map,設置初始值。
get方法
public T get() {
    Thread t = Thread.currentThread();//得到當前線程對象
    ThreadLocalMap map = getMap(t);//線程對象對應的map
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);// 以當前threadlocal爲key,嘗試得到實體
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 若是當前線程對應map不存在
    // 若是map存在可是當前threadlocal沒有關連的entry。
    return 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;
}
  1. 先嚐試得到當前線程,得到當前線程對應的map。
  2. 若是得到的map不爲空,以當前threadlocal爲key嘗試得到entry。
  3. 若是entry不爲空,返回值。
  4. 但凡2跟3 出現沒法得到則經過initialValue函數得到初始值,而後給當前線程建立新map。
remove

首先嚐試獲取當前線程,而後根據當前線程得到map,從map中嘗試刪除enrty。

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
initialValue
  1. 若是沒有調用set直接get,則會調用此方法,該方法只會被調用一次,
  2. 返回一個缺省值null。
  3. 若是不想返回null,能夠Override 進行覆蓋。
   protected T initialValue() {
        return null;
    }

ThreadLocalMap源碼分析

在分析ThreadLocal重要方法時,能夠知道ThreadLocal的操做都是圍繞ThreadLocalMap展開的,其中2包含3,1包含2。

  1. public class ThreadLocal
  2. static class ThreadLocalMap
  3. static class Entry extends WeakReference<ThreadLocal<?>>
在這裏插入圖片描述

ThreadLocalMap成員變量

跟HashMap同樣的參數,此處再也不重複。

// 跟hashmap相似的一些參數
private static final int INITIAL_CAPACITY = 16;

private Entry[] table;

private int size = 0;

private int threshold; // Default to 0

ThreadLocalMap主要函數:

剛剛說的ThreadLocal中的一些getsetremove方法底層調用的都是下面這幾個函數

set(ThreadLocal,Object)
remove(ThreadLocal)
getEntry(ThreadLocal)

內部類Entry

// Entry 繼承子WeakReference,而且key 必須說ThreadLocal
// 若是key是null,意味着key再也不被引用,這是好entry能夠從table清除
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

ThreadLocalMap中,用Entry來保存KV結構,同時Entry中的key(Threadlocal)是弱引用,目的是將ThreadLocal對象生命週期跟線程週期解綁。弱引用:

WeakReference :一些有用(程度比軟引用更低)可是並不是必需,用弱引用關聯的對象,只能生存到下一次垃圾回收以前,GC發生時,無論內存夠不夠,都會被回收。

弱引用跟內存泄漏

可能有些人認爲使用ThreadLocal的過程當中發生了內存泄漏跟Entry中使用弱引用key有關,結論是不對的。

若是Key是強引用
  1. 若是在業務代碼中使用完 ThreadLocal則此時,Stack中的 ThreadLocalRef就會被 回收了。
  2. 可是此時 ThreadLocalMap中的Entry中的Key是強引用 ThreadLocal的,會形成 ThreadLocal實例 沒法回收
  3. 若是咱們沒有刪除Entry而且CurrentThread依然運行的狀況下,強引用鏈以下圖紅色,會致使Entry內存泄漏。
在這裏插入圖片描述

結論: 強引用沒法避免內存泄漏。

若是key是弱引用
  1. 若是在業務代碼中使用完來 ThreadLocal則此時,Stack中的 ThreadLocalRef就會被 回收了。
  2. 可是此時 ThreadLocalMap中的Entry中的Key是弱引用 ThreadLocal的,會形成 ThreadLocal回收,此時Entry中的key = null。
  3. 可是當咱們沒有手動刪除Entry以及CurrentThread依然運行的時候仍是存在強引用鏈,由於 ThreadLocalRef已經被回收了,那麼此時的value就沒法訪問到了,致使value內存泄漏!
在這裏插入圖片描述

結論:弱引用也沒法避免內存泄漏。

內存泄漏緣由

上面分析後知道內存泄漏跟強/弱應用無關,內存泄漏的前提有兩個。

  1. ThreadLocalRef用完後 Entry沒有手動刪除。
  2. ThreadLocalRef用完後 CurrentThread依然在運行ing。
  • 第一點代表當咱們在使用完畢 ThreadLocal後,調用其對應的 remove方法刪除對應的 Entry就能夠避免內存泄漏。
  • 第二點是因爲 ThreadLocalMapCurrentThread的一個屬性,被當前線程引用,生命週期跟 CurrentThread同樣,若是當前線程結束 ThreadLocalMap被回收,天然裏面的Entry也被回收了,單問題是若是此時的線程不同會被回收啊!,若是是線程池呢,用完就放回池子裏了。

結論:ThreadLocal內存泄漏根源是因爲ThreadLocalMap生命週期跟Thread同樣,若是用完ThreadLocal沒有手動刪除就回內存泄漏。

爲何用弱引用

前面分析後知道內存泄漏跟強弱引用無關,那麼爲何還要用弱引用?咱們知道避免內存泄漏的方式有兩個。

  1. ThreadLocal使用完畢後調用 remove方法刪除對應的Entry。
  2. ThreadLocal使用完畢後,當前的 Thread也隨之結束。

第一種方法容易實現,第二站很差搞啊!尤爲是若是線程是從線程池拿的用完後是要放回線程池的,不會被銷燬。

事實上在ThreadLocalMap中的set/getEntry方法中,咱們會對key = null (也就是ThreadLocal爲null)進行斷定,若是key = null,則系統認爲value沒用了也會設置爲null。

這意味着當咱們使用完畢ThreadLocalThread仍然運行的前提下即便咱們忘記調用remove, 弱引用也會比強引用多一層保障,弱引用的ThreadLocal會被收回而後key就是null了,對應的value會在咱們下一次調用ThreadLocalset/get/remove任意一個方法的時候都會調用到底層ThreadLocalMap中的對應方法。無用的value會被清除從而避免內存泄漏。對應的具體函數爲expungeStaleEntry

Hash衝突

構造方法

咱們看下ThreadLocalMap構造方法:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];//新建table
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //找到位置
    table[i] = new Entry(firstKey, firstValue);//放置新的entry
    size = 1;// 容量初始化
    setThreshold(INITIAL_CAPACITY);// 設置擴容閾值
}

threadLocalHashCode = nextHashCode();

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

private static AtomicInteger nextHashCode =
    new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;
// 避免哈希衝突儘可能

其實構造方法跟位細節運算看HashMap,寫過的再也不重複。

set方法

流程大體以下:

  1. 根據key得到對應的索引i,查找i位置上的Entry
  2. 若是Entry已存在並key也相等則直接進行值的覆蓋。
  3. 若是Entry存在,可是key爲空,調用 replaceStaleEntry替換key爲空的Entry
  4. 若是遇到了 table[i]爲null的時候則須要在 table[i]出建立一個新的Entry,而且插入,同時size+1。
  5. 調用 cleanSomeSlots清理key爲null的Entry,再 rehash


private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);//計算索引位置

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) { // 開放定值法解決哈希衝突
        ThreadLocal<?> k = e.get();

        if (k == key) {//直接覆蓋
            e.value = value;
            return;
        }

        if (k == null) {// 若是key不是空value是空,垃圾清除內存泄漏防止。
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 若是ThreadLocal對應的key不存在而且沒找到舊元素,則在空元素位置建立個新Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

// 環形數組 下一個索引
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

PS:

  1. 每一個ThreadLocal只能保存一個變量 副本,若是想要上線一個線程可以保存多個副本以上,就須要建立多個ThreadLocal。
  2. ThreadLocal內部的ThreadLocalMap鍵爲 引用,會有內存泄漏的風險,用完記得擦屁股。
  3. 適用於無狀態,副本變量獨立後不影響業務邏輯的高併發場景。若是若是業務邏輯強依賴於副本變量,則不適合用ThreadLocal解決,須要另尋解決方案

若是想共享線程的ThreadLocal數據怎麼辦?

使用 InheritableThreadLocal 能夠實現多個線程訪問ThreadLocal的值,咱們在主線程中建立一個InheritableThreadLocal的實例,而後在子線程中獲得這個InheritableThreadLocal實例設置的值。

private void test() {    
final ThreadLocal threadLocal = new InheritableThreadLocal();       
threadLocal.set("帥得一匹");    
Thread t = new Thread() {        
    @Override        
    public void run() {            
      super.run();            
      Log.i( "張三帥麼 =" + threadLocal.get());        
    }    
  };          
  t.start(); 

爲何通常用ThreadLocal都要用Static?

阿里規範有云:

ThreadLocal沒法解決共享對象的更新問題,ThreadLocal對象建議使用 static修飾。這個變量是針對一個線程內全部操做共享的,因此設置爲靜態變量,全部此類實例共享此靜態變量 ,也就是說在類第一次被使用時裝載,只分配一塊存儲空間,全部此類的對象(只要是這個線程內定義的)均可以操控這個變量。

JDK官方規範有云:

參考

黑馬老師講解


本文分享自微信公衆號 - sowhat1412(sowhat9094)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索