1. 程式人生 > >ThreadLocal 內部實現、應用場景和記憶體洩漏

ThreadLocal 內部實現、應用場景和記憶體洩漏

一、什麼是ThreadLocal

首先明確一個概念,那就是ThreadLocal並不是用來併發控制訪問一個共同物件,而是為了給每個執行緒分配一個只屬於該執行緒的變數,顧名思義它是local variable(執行緒區域性變數)。它的功用非常簡單,就是為每一個使用該變數的執行緒都提供一個變數值的副本,是每一個執行緒都可以獨立地改變自己的副本,而不會和其它執行緒的副本衝突,實現執行緒間的資料隔離。從執行緒的角度看,就好像每一個執行緒都完全擁有該變數。

set和get方法是ThreadLocal類中最常用的兩個方法。,接下來 我們來看下ThreadLocal的內部實現:

set方法實現原始碼如下:

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }


ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

//Thread類裡預設threadLocals為null
class Thread implements Runnable{ ThreadLocal.ThreadLocalMap threadLocals = null; } static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super
(k); value = v; } } } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }

Thread.currentThread得到當前執行緒,如果當前執行緒存在threadLocals這個變數不為空,那麼根據當前的ThreadLocal例項作為key尋找在map中位置,然後用新的value值來替換舊值。

在ThreadLocal這個類中比較引人注目的應該是ThreadLocal->ThreadLocalMap->Entry這個類。這個類繼承自WeakReference。

get方法實現原始碼如下:

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        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;
    }

首先我們通過Thread.currentThread得到當前執行緒,然後獲取當前執行緒的threadLocals變數,這個變數就是ThreadLocalMap型別的,如果這個變數map不為空,再獲取ThreadLocalMap.Entry e,如果e不為空,則獲取value值返回,否則在Map中初始化Entry,並返回初始值null。如果map為空,則建立並初始化map,並返回初始值null。

二、ThreadLocal應用場景

1、資料庫連線池實現

jdbc連線資料庫,如下所示:

Class.forName("com.mysql.jdbc.Driver");
java.sql.Connection conn = DriverManager.getConnection(jdbcUrl);

注意:一次Drivermanager.getConnection(jdbcurl)獲得只是一個connection,並不能滿足高併發情況。因為connection不是執行緒安全的,一個connection對應的是一個事物。

每次獲得connection都需要浪費cpu資源和記憶體資源,是很浪費資源的。所以誕生了資料庫連線池。資料庫連線池實現原理如下:

pool.getConnection(),都是先從threadlocal裡面拿的,如果threadlocal裡面有,則用,保證執行緒裡的多個dao操作,用的是同一個connection,以保證事務。如果新執行緒,則將新的connection放在threadlocal裡,再get給到執行緒。

將connection放進threadlocal裡的,以保證每個執行緒從連線池中獲得的都是執行緒自己的connection。

Hibernate的資料庫連線池原始碼實現:

 public class ConnectionPool implements IConnectionPool {  
    // 連線池配置屬性  
    private DBbean dbBean;  
    private boolean isActive = false; // 連線池活動狀態  
    private int contActive = 0;// 記錄建立的總的連線數  

    // 空閒連線  
    private List<Connection> freeConnection = new Vector<Connection>();  
    // 活動連線  
    private List<Connection> activeConnection = new Vector<Connection>();  

 // 將執行緒和連線繫結,保證事務能統一執行
    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>(); 

public ConnectionPool(DBbean dbBean) {  
        super();  
        this.dbBean = dbBean;  
        init();  
        cheackPool();  
    }  

    // 初始化  
    public void init() {  
        try {  
            Class.forName(dbBean.getDriverName());  
            for (int i = 0; i < dbBean.getInitConnections(); i++) {  
                Connection conn;  
                conn = newConnection();  
                // 初始化最小連線數  
                if (conn != null) {  
                    freeConnection.add(conn);  
                    contActive++;  
                }  
            }  
            isActive = true;  
        } catch (ClassNotFoundException e) {  
            e.printStackTrace();  
        } catch (SQLException e) {  
            e.printStackTrace();  
        }  
    }  

    // 獲得當前連線  
    public Connection getCurrentConnecton(){  
        // 預設執行緒裡面取  
        Connection conn = threadLocal.get();  
        if(!isValid(conn)){  
            conn = getConnection();  
        }  
        return conn;  
    }  

    // 獲得連線  
    public synchronized Connection getConnection() {  
        Connection conn = null;  
        try {  
            // 判斷是否超過最大連線數限制  
            if(contActive < this.dbBean.getMaxActiveConnections()){  
                if (freeConnection.size() > 0) {  
                    conn = freeConnection.get(0);  
                    if (conn != null) {  
                        threadLocal.set(conn);  
                    }  
                    freeConnection.remove(0);  
                } else {  
                    conn = newConnection();  
                }  

            }else{  
                // 繼續獲得連線,直到從新獲得連線  
                wait(this.dbBean.getConnTimeOut());  
                conn = getConnection();  
            }  
            if (isValid(conn)) {  
                activeConnection.add(conn);  
                contActive ++;  
            }  
        } catch (SQLException e) {  
            e.printStackTrace();  
        } catch (ClassNotFoundException e) {  
            e.printStackTrace();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        return conn;  
    }  

    // 獲得新連線  
    private synchronized Connection newConnection()  
            throws ClassNotFoundException, SQLException {  
        Connection conn = null;  
        if (dbBean != null) {  
            Class.forName(dbBean.getDriverName());  
            conn = DriverManager.getConnection(dbBean.getUrl(),  
                    dbBean.getUserName(), dbBean.getPassword());  
        }  
        return conn;  
    }  

    // 釋放連線  
    public synchronized void releaseConn(Connection conn) throws SQLException {  
        if (isValid(conn)&& !(freeConnection.size() > dbBean.getMaxConnections())) {  
            freeConnection.add(conn);  
            activeConnection.remove(conn);  
            contActive --;  
            threadLocal.remove();  
            // 喚醒所有正待等待的執行緒,去搶連線  
            notifyAll();  
        }  
    }  

    // 判斷連線是否可用  
    private boolean isValid(Connection conn) {  
        try {  
            if (conn == null || conn.isClosed()) {  
                return false;  
            }  
        } catch (SQLException e) {  
            e.printStackTrace();  
        }  
        return true;  
    }  

    // 銷燬連線池  
    public synchronized void destroy() {  
        for (Connection conn : freeConnection) {  
            try {  
                if (isValid(conn)) {  
                    conn.close();  
                }  
            } catch (SQLException e) {  
                e.printStackTrace();  
            }  
        }  
        for (Connection conn : activeConnection) {  
            try {  
                if (isValid(conn)) {  
                    conn.close();  
                }  
            } catch (SQLException e) {  
                e.printStackTrace();  
            }  
        }  
        isActive = false;  
        contActive = 0;  
    }  

    // 連線池狀態  
    @Override  
    public boolean isActive() {  
        return isActive;  
    }  

    // 定時檢查連線池情況  
    @Override  
    public void cheackPool() {  
        if(dbBean.isCheakPool()){  
            new Timer().schedule(new TimerTask() {  
            @Override  
            public void run() {  
            // 1.對執行緒裡面的連線狀態  
            // 2.連線池最小 最大連線數  
            // 3.其他狀態進行檢查,因為這裡還需要寫幾個執行緒管理的類,暫時就不添加了  
            System.out.println("空線池連線數:"+freeConnection.size());  
            System.out.println("活動連線數::"+activeConnection.size());  
            System.out.println("總的連線數:"+contActive);  
                }  
            },dbBean.getLazyCheck(),dbBean.getPeriodCheck());  
        }  
    }  
}  

2、有時候ThreadLocal也可以用來避免一些引數傳遞,通過ThreadLocal來訪問物件。

比如一個方法呼叫另一個方法時傳入了8個引數,通過逐層呼叫到第N個方法,傳入了其中一個引數,此時最後一個方法需要增加一個引數,第一個方法變成9個引數是自然的,但是這個時候,相關的方法都會受到牽連,使得程式碼變得臃腫不堪。這時候就可以將要新增的引數設定成執行緒本地變數,來避免參數傳遞。

上面提到的是ThreadLocal一種亡羊補牢的用途,不過也不是特別推薦使用的方式,它還有一些類似的方式用來使用,就是在框架級別有很多動態呼叫,呼叫過程中需要滿足一些協議,雖然協議我們會盡量的通用,而很多擴充套件的引數在定義協議時是不容易考慮完全的以及版本也是隨時在升級的,但是在框架擴充套件時也需要滿足介面的通用性和向下相容,而一些擴充套件的內容我們就需要ThreadLocal來做方便簡單的支援。

簡單來說,ThreadLocal是將一些複雜的系統擴充套件變成了簡單定義,使得相關引數牽連的部分變得非常容易。

3、在某些情況下提升效能和安全。

用SimpleDateFormat這個物件,進行日期格式化。因為建立這個物件本身很費時的,而且我們也知道SimpleDateFormat本身不是執行緒安全的,也不能快取一個共享的SimpleDateFormat例項,為此我們想到使用ThreadLocal來給每個執行緒快取一個SimpleDateFormat例項,提高效能。同時因為每個Servlet會用到不同pattern的時間格式化類,所以我們對應每一種pattern生成了一個ThreadLocal例項。

public interface DateTimeFormat {
        String DATE_PATTERN = "yyyy-MM-dd";
        ThreadLocal<DateFormat> DATE_FORMAT = ThreadLocal.withInitial(() -> {
            return new SimpleDateFormat("yyyy-MM-dd");
        });
        String TIME_PATTERN = "HH:mm:ss";
        ThreadLocal<DateFormat> TIME_FORMAT = ThreadLocal.withInitial(() -> {
            return new SimpleDateFormat("HH:mm:ss");
        });
        String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
        ThreadLocal<DateFormat> DATE_TIME_FORMAT = ThreadLocal.withInitial(() -> {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        });
    }

為什麼SimpleDateFormat不安全,可以參考此篇博文:

假如我們把SimpleDateFormat定義成static成員變數,那麼多個thread之間會共享這個sdf物件, 所以Calendar物件也會共享。
假定執行緒A和執行緒B都進入了parse(text, pos) 方法, 執行緒B執行到calendar.clear()後,執行緒A執行到calendar.getTime(), 那麼就會有問題。

如果不用static修飾,將SimpleDateFormat定義成區域性變數:
每呼叫一次方法就會建立一個SimpleDateFormat物件,方法結束又要作為垃圾回收。加鎖效能較差,每次都要等待鎖釋放後其他執行緒才能進入。那麼最好的辦法就是:使用ThreadLocal: 每個執行緒都將擁有自己的SimpleDateFormat物件副本。

附-SimpleDateFormat關鍵原始碼:

public class SimpleDateFormat extends DateFormat {  

    public Date parse(String text, ParsePosition pos){  
        calendar.clear(); // Clears all the time fields  
        // other logic ...  
        Date parsedDate = calendar.getTime();  
    }  
}  

abstract class DateFormat{  
    // other logic ...  
    protected Calendar calendar;  
    public Date parse(String source) throws ParseException{  
        ParsePosition pos = new ParsePosition(0);  
        Date result = parse(source, pos);  
        if (pos.index == 0)  
            throw new ParseException("Unparseable date: \"" + source + "\"" ,  
                pos.errorIndex);  
        return result;  
    }  
}  

三、記憶體洩漏問題

在上面提到過,每個thread中都存在一個map, map的型別是ThreadLocal.ThreadLocalMap. Map中的key為一個threadlocal例項. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向threadlocal. 當把threadlocal例項置為null以後,沒有任何強引用指向threadlocal例項,所以threadlocal將會被gc回收. 但是,我們的value卻不能回收,因為存在一條從current thread連線過來的強引用. 只有當前thread結束以後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將全部被GC回收。

所以得出一個結論就是隻要這個執行緒物件被gc回收,就不會出現記憶體洩露,但在threadLocal設為null和執行緒結束這段時間不會被回收的,就發生了我們認為的記憶體洩露。其實這是一個對概念理解的不一致,也沒什麼好爭論的。最要命的是執行緒物件不被回收的情況,這就發生了真正意義上的記憶體洩露。比如使用執行緒池的時候,執行緒結束是不會銷燬的,會再次使用的。就可能出現記憶體洩露。

相關推薦

ThreadLocal 內部實現應用場景記憶體洩漏

一、什麼是ThreadLocal 首先明確一個概念,那就是ThreadLocal並不是用來併發控制訪問一個共同物件,而是為了給每個執行緒分配一個只屬於該執行緒的變數,顧名思義它是local variable(執行緒區域性變數)。它的功用非常簡單,就是為每

資料庫各派系起源應用場景選擇指南

from:http://tech.it168.com/a2015/0303/1708/000001708320.shtml 一、縱覽各種資料模型   這些模型的分類方法來自於Emil Eifrem 和 NoSQL databases。   1. 文件資料庫   

裸金屬(Ironic): 一種雲基礎架構應用場景趨勢解析

      如今Openstack在虛擬化管理部分已經很成熟了,通過Nova我們可以建立虛擬機器

C#——委託Lambda表示式閉包記憶體洩漏

使用委託的典型情況 首先看看委託的常見的使用情景:定義一個委託、使用剛定義的委託宣告一個委託變數、根據需要將方法和該變數繫結,最後在合適的地方使用它。程式碼形式如下: //定義委託 public delegate void SomeDelegate(); cla

ThreadLocal 內部實現應用場景

很多人都知道java中有ThreadLocal這個類,但是知道ThreadLocal這個類具體有什麼作用,然後適用什麼樣的業務場景還是很少的。今天我就嘗試以自己的理解,來講解下ThreadLocal類的內部實現和應用場景,如果有什麼不對之處,還望大家指正。 首先明確一個

<Golang>MD5SHA256等雜湊演算法介紹應用場景及具體實現

版權宣告:本文為作者原創,如需轉載,請註明出處https://blog.csdn.net/weixin_42940826 前言 MD5和SHA256是非常常用的兩種單向雜湊函式,雖然MD5在2005年已經被中國密碼學家王小云攻破,但是曾經也是叱吒風雲的被大規模使用,現在

ThreadLocal實現原理記憶體洩漏問題

1.概述 ThreadLocal不是為了解決多執行緒訪問共享變數,而是為每個執行緒建立一個單獨的變數副本,變數在多執行緒環境下訪問(通過get或set方法訪問)時能保證各個執行緒裡的變數相對獨立於其他執行緒內的變數,ThreadLocal例項通常來說都是private static型別。

bind函式作用應用場景以及模擬實現

bind函式 bind 函式掛在 Function 的原型上 Function.prototype.bind 建立的函式都可以直接呼叫 bind,使用: function func(){ console.log(this) }

cas實現單點登入-應用場景完整配置

Cas 簡介 1、什麼是CAS CAS是一個單點登入(SSO)的框架。單點登入是目前比較流行的服務於企業業務整合的解決方案之一,SSO使得在多個應用系統中,使用者只需要登入一次就可以訪問所有相互信任的應用系統。 2、CAS的主要結構 CAS包括兩部分:

kafka的內部實現安裝使用

原理解析producer建立一個topic時,可以指定該topic為幾個partition(預設是1,配置num.partitions),然後會把partition分配到每個broker上,分配的演算法是:a個broker,第b個partition分配到b%a的broker上

限流演算法的理解應用場景實現[臨界點處理]

在開發高併發系統時,有三把利器來保護系統:快取、降級和限流。一下有幾種限流的方法可以參考。 訊號量和令牌桶的區別:     訊號量限制的是併發,資源. 令牌桶如果耗時比較高的話,併發可能會比較大. 限制的是 qps. 計數器法 計數器法是限流演算法裡最簡單也是

包裝類應用場景自動裝箱拆箱

sys art 允許 應用 功能 包裝類 賦值 默認值 方法 包裝類應用場景和自動裝箱、拆箱 1、集合類泛型只能是包裝類; List<Integer> list; 2、成員變量不能有默認值; 基本數據類型的成員變量都有默認值,如以上代碼 status 默

面試官:ThreadLocal應用場景注意事項有哪些?

## 前言 ThreadLocal主要有如下2個作用 1. 保證執行緒安全 2. 線上程級別傳遞變數 ## 保證執行緒安全 最近一個小夥伴把專案中封裝的日期工具類用在多執行緒環境下居然出了問題,來看看怎麼回事吧 日期轉換的一個工具類 ```java public class DateUtil {

ThreadLocal的理解與應用場景分析

位置 理解 原理 解釋 als 存取 cti 只需要 his 對於Java ThreadLocal的理解與應用場景分析 一、對ThreadLocal理解 ThreadLocal提供一個方便的方式,可以根據不同的線程存放一些不同的特征屬性,可以方便的在線程中進行存取。

Docker五種存儲驅動原理及應用場景性能測試對比

Docker 存儲驅動 Docker最開始采用AUFS作為文件系統,也得益於AUFS分層的概念,實現了多個Container可以共享同一個image。但由於AUFS未並入Linux內核,且只支持Ubuntu,考慮到兼容性問題,在Docker 0.7版本中引入了存儲驅動, 目前,Docker支持AUFS

MongoDBHbaseRedis等NoSQL優劣勢應用場景

tel val 開發 一段時間 2.4 緩沖區 sta 位置 date NoSQL的四大種類 NoSQL數據庫在整個數據庫領域的江湖地位已經不言而喻。在大數據時代,雖然RDBMS很優秀,但是面對快速增長的數據規模和日漸復雜的數據模型,RDBMS漸漸力不從心,無法應對很多數據

zookeeper的應用場景相關理論

zk的應用場景 用監聽機制監聽自身的znode的變化 1)命名服務: 全域性統一命名服務 同一個檔案3個副本 修改檔名 怎麼保證3個副本檔名一樣 將全域性統一的命名放在zk的znode的節點的儲存內容上 哪一個客戶端對這個感興趣就可以新增監聽 2)配置檔案管理 安裝hadoop叢集的時候 叢

MongoDBHbaseRedis等NoSQL優劣勢應用場景 NoSQL的四大種類

NoSQL資料庫在整個資料庫領域的江湖地位已經不言而喻。在大資料時代,雖然RDBMS很優秀,但是面對快速增長的資料規模和日漸複雜的資料模型,RDBMS漸漸力不從心,無法應對很多資料庫處理任務,這時NoSQL憑藉易擴充套件、大資料量和高效能以及靈活的資料模型成功的在資料庫領域站穩了腳跟。 目前大家

記憶體溢位記憶體洩漏的區別產生原因以及解決方案【轉】

(轉自:https://www.cnblogs.com/Sharley/p/5285045.html) 記憶體溢位 out of memory,是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory;比如申請了一個integer,但給它存了long才能存下的數,那就

行人重識別(ReID) ——技術實現應用場景

導讀 跨鏡追蹤(Person Re-Identification,簡稱 ReID)技術是現在計算機視覺研究的熱門方向,主要解決跨攝像頭跨場景下行人的識別與檢索。該技術能夠根據行人的穿著、體態、髮型等資訊認知行人,與人臉識別結合能夠適用於更多新的應用場景,將人工智慧的認知水平提高到一個新階段。