正確理解ThreadLocal

想必很多朋友對 ThreadLocal并不陌生,今天我們就來一起探討下ThreadLocal的使用方法和實現原理。首先,本文先談一下對ThreadLocal的理 解,然后根據ThreadLocal類的源碼分析了其實現原理和使用需要注意的地方,最后給出了兩個應用場景。

  以下是本文目錄大綱:

  一.對ThreadLocal的理解

  二.深入解析ThreadLocal類

  三.ThreadLocal的應用場景

  若有不正之處請多多諒解,并歡迎批評指正。

  請尊重作者勞動成果,轉載請標明原文鏈接:

?  http://www.cnblogs.com/dolphin0520/p/3920407.html

一.對ThreadLocal的理解

  ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲,其實意思差不多。可能很多朋友都知道ThreadLocal為變量在每個線程中都創建了一個副本,那么每個線程可以訪問自己內部的副本變量。

  這句話從字面上看起來很容易理解,但是真正理解并不是那么容易。

  我們還是先來看一個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ConnectionManager {
?????
????private static Connection connect = null;
?????
????public static Connection openConnection() {
????????if(connect == null){
????????????connect = DriverManager.getConnection();
????????}
????????return connect;
????}
?????
????public static void closeConnection() {
????????if(connect!=null)
????????????connect.close();
????}
}

?  假設有這樣一個數據庫鏈接管理類,這段代碼在單線程中使用是沒有任何問題的,但是如果在多線程中使用呢?很顯然,在多線程中使用會存在線程安 全問題:第一,這里面的2個方法都沒有進行同步,很可能在openConnection方法中會多次創建connect;第二,由于connect是共享 變量,那么必然在調用connect的地方需要使用到同步來保障線程安全,因為很可能一個線程在使用connect進行數據庫操作,而另外一個線程調用 closeConnection關閉鏈接。

  所以出于線程安全的考慮,必須將這段代碼的兩個方法進行同步處理,并且在調用connect的地方需要進行同步處理。

  這樣將會大大影響程序執行效率,因為一個線程在使用connect進行數據庫操作的時候,其他線程只有等待。

  那么大家來仔細分析一下這個問題,這地方到底需不需要將connect變量進行共享?事實上,是不需要的。假如每個線程中都有一個 connect變量,各個線程之間對connect變量的訪問實際上是沒有依賴關系的,即一個線程不需要關心其他線程是否對這個connect進行了修改 的。

  到這里,可能會有朋友想到,既然不需要在線程之間共享這個變量,可以直接這樣處理,在每個需要使用數據庫連接的方法中具體使用時才創建數據庫鏈接,然后在方法調用完畢再釋放這個連接。比如下面這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ConnectionManager {
?????
????private? Connection connect = null;
?????
????public Connection openConnection() {
????????if(connect == null){
????????????connect = DriverManager.getConnection();
????????}
????????return connect;
????}
?????
????public void closeConnection() {
????????if(connect!=null)
????????????connect.close();
????}
}
class Dao{
????public void insert() {
????????ConnectionManager connectionManager = new ConnectionManager();
????????Connection connection = connectionManager.openConnection();
?????????
????????//使用connection進行操作
?????????
????????connectionManager.closeConnection();
????}
}

?  這樣處理確實也沒有任何問題,由于每次都是在方法內部創建的連接,那么線程之間自然不存在線程安全問題。但是這樣會有一個致命的影響:導致服 務器壓力非常大,并且嚴重影響程序執行性能。由于在方法中需要頻繁地開啟和關閉數據庫連接,這樣不盡嚴重影響程序執行效率,還可能導致服務器壓力巨大。

  那么這種情況下使用ThreadLocal是再適合不過的了,因為ThreadLocal在每個線程中對該變量會創建一個副本,即每個線程內部 都會有一個該變量,且在線程內部任何地方都可以使用,線程之間互不影響,這樣一來就不存在線程安全問題,也不會嚴重影響程序執行性能。

  但是要注意,雖然ThreadLocal能夠解決上面說的問題,但是由于在每個線程中都創建了副本,所以要考慮它對資源的消耗,比如內存的占用會比不使用ThreadLocal要大。

二.深入解析ThreadLocal類

  在上面談到了對ThreadLocal的一些理解,那我們下面來看一下具體ThreadLocal是如何實現的。

  先了解一下ThreadLocal類提供的幾個方法:

1
2
3
4
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }

?  get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本,set()用來設置當前線程中變量的副本,remove()用 來移除當前線程中變量的副本,initialValue()是一個protected方法,一般是用來在使用時進行重寫的,它是一個延遲加載方法,下面會 詳細說明。

  首先我們來看一下ThreadLocal類是如何為每個線程創建一個變量的副本的。

  先看下get方法的實現:

  

?  第一句是取得當前線程,然后通過getMap(t)方法獲取到一個map,map的類型為ThreadLocalMap。然后接著下面獲取到<key,value>鍵值對,注意這里獲取鍵值對傳進去的是? this,而不是當前線程t。

  如果獲取成功,則返回value值。

  如果map為空,則調用setInitialValue方法返回value。

  我們上面的每一句來仔細分析:

  首先看一下getMap方法中做了什么:

  

  可能大家沒有想到的是,在getMap中,是調用當期線程t,返回當前線程t中的一個成員變量threadLocals。

  那么我們繼續取Thread類中取看一下成員變量threadLocals是什么:

  

  實際上就是一個ThreadLocalMap,這個類型是ThreadLocal類的一個內部類,我們繼續取看ThreadLocalMap的實現:

  

  可以看到ThreadLocalMap的Entry繼承了WeakReference,并且使用ThreadLocal作為鍵值。

  然后再繼續看setInitialValue方法的具體實現:

  很容易了解,就是如果map不為空,就設置鍵值對,為空,再創建Map,看一下createMap的實現:

  

  至此,可能大部分朋友已經明白了ThreadLocal是如何為每個線程創建變量的副本的:

  首先,在每個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個 threadLocals就是用來存儲實際的變量副本的,鍵值為當前ThreadLocal變量,value為變量副本(即T類型的變量)。

  初始時,在Thread里面,threadLocals為空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對 Thread類中的threadLocals進行初始化,并且以當前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為 value,存到threadLocals。

  然后在當前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找。

  下面通過一個例子來證明通過ThreadLocal能達到在每個線程中創建變量副本的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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(test.getLong());
????????System.out.println(test.getString());
?????
?????????
????????Thread thread1 = new Thread(){
????????????public void run() {
????????????????test.set();
????????????????System.out.println(test.getLong());
????????????????System.out.println(test.getString());
????????????};
????????};
????????thread1.start();
????????thread1.join();
?????????
????????System.out.println(test.getLong());
????????System.out.println(test.getString());
????}
}

?  這段代碼的輸出結果為:

  

  從這段代碼的輸出結果可以看出,在main線程中和thread1線程中,longLocal保存的副本值和stringLocal保存的副本值都不一樣。最后一次在main線程再次打印副本值是為了證明在main線程中和thread1線程中的副本值確實是不同的。

  總結一下:

  1)實際的通過ThreadLocal創建的副本是存儲在每個線程自己的threadLocals中的;

  2)為何threadLocals的類型ThreadLocalMap的鍵值為ThreadLocal對象,因為每個線程中可有多個threadLocal變量,就像上面代碼中的longLocal和stringLocal;

  3)在進行get之前,必須先set,否則會報空指針異常;

  ??? 如果想在get之前不需要調用set就能正常訪問的話,必須重寫initialValue()方法。

    因為在上面的代碼分析過程中,我們發現如果沒有先set的話,即在map中查找不到對應的存儲,則會通過調用setInitialValue方法返回i, 而在setInitialValue方法中,有一個語句是T value = initialValue(), 而默認情況下,initialValue方法返回的是null。

  

  看下面這個例子:

  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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();
?????????
????????System.out.println(test.getLong());
????????System.out.println(test.getString());
????????Thread thread1 = new Thread(){
????????????public void run() {
????????????????test.set();
????????????????System.out.println(test.getLong());
????????????????System.out.println(test.getString());
????????????};
????????};
????????thread1.start();
????????thread1.join();
?????????
????????System.out.println(test.getLong());
????????System.out.println(test.getString());
????}
}

?  在main線程中,沒有先set,直接get的話,運行時會報空指針異常。

  但是如果改成下面這段代碼,即重寫了initialValue方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Test {
????ThreadLocal<Long> longLocal = new ThreadLocal<Long>(){
????????protected Long initialValue() {
????????????return Thread.currentThread().getId();
????????};
????};
????ThreadLocal<String> stringLocal = new ThreadLocal<String>(){;
????????protected String initialValue() {
????????????return Thread.currentThread().getName();
????????};
????};
?????
????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(test.getLong());
????????System.out.println(test.getString());
?????
?????????
????????Thread thread1 = new Thread(){
????????????public void run() {
????????????????test.set();
????????????????System.out.println(test.getLong());
????????????????System.out.println(test.getString());
????????????};
????????};
????????thread1.start();
????????thread1.join();
?????????
????????System.out.println(test.getLong());
????????System.out.println(test.getString());
????}
}

?  就可以直接不用先set而直接調用get了。

三.ThreadLocal的應用場景

  最常見的ThreadLocal使用場景為 用來解決 數據庫連接、Session管理等。

  如:

1
2
3
4
5
6
7
8
9
10
private static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
public Connection initialValue() {
????return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}

?  下面這段代碼摘自:

  http://www.iteye.com/topic/103804

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

?

  參考資料:

  《深入理解Java虛擬機》

  《Java編程思想》

  http://ifeve.com/thread-management-10/

  http://www.ibm.com/developerworks/cn/java/j-threads/index3.html

  http://www.iteye.com/topic/103804

  http://www.iteye.com/topic/777716

  http://www.iteye.com/topic/757478

  http://blog.csdn.net/ghsau/article/details/15732053

  http://ispring.iteye.com/blog/162982

  http://blog.csdn.net/imzoer/article/details/8262101

  http://www.blogjava.net/wumi9527/archive/2010/09/10/331654.html

  http://bbs.csdn.net/topics/380049261

?

轉載來自:作者:海子
出處:http://www.cnblogs.com/dolphin0520/

轉載于:https://www.cnblogs.com/suiyueqiannian/p/5961452.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/397543.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/397543.shtml
英文地址,請注明出處:http://en.pswp.cn/news/397543.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

2018.7.10 個人博客文章=利用ORM創建分類和ORM的內置函數

昨天的注冊收尾工作 其實就差了和MySql聯系起來的部分&#xff0c;這部分很簡單&#xff0c;首先要做的就是保存用戶通過from傳送過來的頭像文件&#xff1a; """ 保存頭像文件 """ file request.FILES.get(avatar) file_path os.path.join(st…

python 列表與元組的操作簡介

上一篇&#xff1a;Python 序列通用操作介紹 列表 列表是可變的(mutable)——可以改變列表的內容&#xff0c;這不同于字符串和元組&#xff0c;字符串和元組都是不可變的。接下來討論一下列表所提供的方法。 list函數 可以使用list函數來創建列表&#xff1a; list(Hello) [H,…

mfc嵌入matlab繪圖窗口,將matlab的圖嵌入MFC

【實例簡介】VS調用matlab畫圖模塊編譯成的動態鏈接庫&#xff0c;并在MFC顯示。【實例截圖】【核心代碼】3b0582a3-4ea8-4a61-ba33-e448be563b88└── 將matlab的圖嵌入MFC├── matlab_2010b與VS2008_混合編程的實現.pdf├── TestWithData│ ├── Debug│ │ ├─…

python multiprocessing 和tcp

#用類方法 服務端from socket import *from multiprocessing import Processimport osclass Myprocess(Process): def __init__(self, conn): self.conn conn super().__init__() def run(self): conn self.conn start True whil…

matlab 畫三維花瓶,精美花瓶建模教程

1、首先&#xff0c;草圖單位為mm&#xff0c;進入前視圖繪制如圖草圖&#xff0c;花瓶的基本形狀輪廓2、然后對草圖進行旋轉3、旋轉出曲面后&#xff0c;在頂部邊線新建一個基準面4、繼續在前視圖繪制草圖&#xff0c;如圖繪制一弧線5、然后進行旋轉6、可以得到圖示的兩個曲面…

PKI系統相關知識點介紹

公鑰基礎設施&#xff08;Public Key Infrastructure&#xff0c;簡稱PKI&#xff09;是目前網絡安全建設的基礎與核心&#xff0c;是電子商務安全實施的基本保障&#xff0c;因此&#xff0c;對PKI技術的研究和開發成為目前信息安全領域的熱點。本文對PKI技術進行了全面的分析…

android 打印java堆棧,Android打印堆棧

java打印堆棧方法一&#xff1a;異常對象打印堆棧Exception e new Exception("this is a log");e.printStackTrace();方法二&#xff1a;Log打印獲取異常的堆棧并打印Log.e(“dump_test”,Log.getStackTraceString(new Throwable()));C\C打印堆棧方法一&#xff1a;…

實際算法項目工程上手日志C/C++

#pragma once 為了保證頭文件只被編譯一次&#xff0c;通常放在頭文件的頂部 #define IN #define OUT #define INOUT 這個只在邏輯上起作用&#xff0c; IN 表示輸入參數&#xff0c;指針指向的值不會修改&#xff1b; OUT 表示輸出參數&#xff0c;指針指向的值會修改&a…

Arduino 控制超聲波測距模塊

一.實物圖 二.例子代碼 用到數字2 和3 引腳,還有兩個就是vcc GND兩個陰腳,用模塊連線比較簡單 轉載于:https://www.cnblogs.com/caoguo/p/4785700.html

Linux安裝source-code-pro字體

2019獨角獸企業重金招聘Python工程師標準>>> 1.下載source-code-pro字體 從GitHub下載 https://github.com/adobe-fonts/source-code-pro/releases 2.解壓文件&#xff0c;將OTF格式的文件夾重新命名一下&#xff0c;這里我命名為source-code-pro&#xff0c;然后將…

dft對稱性 matlab實驗,數字信號處理實驗指導書(審)

(0???2?)上對X(ej?)均勻采樣得到?X(k)?X(ej?)??2?k/N??n???x(n)e?j2?kn/N 0?k?N?1可以看到X(k)也是頻域上的有限長序列&#xff0c;長度為N。序列X(k)稱為序列x(n)的N點DFT。N稱為DFT變換區間長度。 通常表示WN?e?j2?/N可將定義式表示為?X(k)??x(n)…

PI

并不是所有東西都可以套PI的&#xff0c;只有滿足上述這類的數學關系才可以。 轉速經過PI調節得到電流也是有原因的。從下圖中可以發現&#xff0c;轉速 k*Iq/s&#xff0c;s是拉普拉斯算子&#xff0c;所以也是滿足積分&#xff0c;比例關系的。 轉載于:https://www.cnblogs.…

AOP之AspectJ簡單使用

為什么80%的碼農都做不了架構師&#xff1f;>>> 參考文章&#xff1a; 使用AspectJ在Android中實現Aop 深入理解Android之AOP自動打印日志主要知識點&#xff1a; 主要是JPoint、pointcuts、advice以及他們之間的關系可以通過aj文件、或AspectJ注解的Java文件實現A…

matlab drawnow連成曲線,precision recall曲線Matlab實現

在用哈希進行檢索時&#xff0c;常會用到precision recall曲線對其性能進行定量評價。precision recall的定義在信息檢索評價指標中已做了詳細說明&#xff0c;這里再記錄一下precision recall的具體實現。precision recall曲線matlab一般使用的都是下面的版本&#xff1a;func…

trap

http://blog.csdn.net/elbort/article/details/8525599 http://mywiki.wooledge.org/SignalTrap轉載于:https://www.cnblogs.com/flowjacky/p/4785723.html

WinSCP實現Ubuntu與 Windows 文件共享方法

2019獨角獸企業重金招聘Python工程師標準>>> WinSCP是一個Windows環境下使用SSH的開源圖形化SFTP客戶端。同時支持SCP協議。它的主要功能就是在本地與遠程計算機間安全的復制文件。WinSCP綠色中文版 一款基于SSH安全高效的FTP上傳軟件。WinSCP 可以執行所有基本的文…

緩存機制

緩存 緩存就是數據交換的緩沖區&#xff08;稱作Cache&#xff09; 客戶端&#xff1a;緩存&#xff08;expires&#xff09;、deflate壓縮 緩存服務器&#xff1a;CDN/cache緩存靜態內容如&#xff1a;html、jpg、gif、js等 靜態web服務器&#xff1a;Apache/nginx靜態服務器提…

Shell學習總結

Shell 是什么&#xff1f; Shell 是一個用C語言編寫的程序&#xff0c;它是用戶使用Linux的橋梁。Shell既是一種命令語言&#xff0c;又是一種程序設計語言。 Shell 是指一種應用程序&#xff0c;這個應用程序提供了一個界面&#xff0c;用戶通過這個界面訪問操作系統內核的服務…

java有幾個關鍵字,Java多線程常用的幾個關鍵字

Java多線程常用的幾個關鍵字二、volatile作用&#xff1a;volatile關鍵字的作用是&#xff1a;使變量在多個線程間可見(具有可見性)&#xff0c;但是僅靠volatile是不能保證線程的安全性&#xff0c;volatile關鍵字不具備synchronized關鍵字的原子性。Demo1:package com.ietree…

PHP獲取QQ等級,php仿QQ等級太陽顯示函數

開頭先引述下QQ等級的算法&#xff1a;設當前等級為N&#xff0c;達到當前等級最少需要的活躍天數為D&#xff0c;當前活躍天數為Dc&#xff0c;升級剩余天數為Dr&#xff0c;則&#xff1a;從而推出:好了&#xff0c;引述完成&#xff0c;懶得寫字了&#xff0c;貼出代碼&…