文档章节

Java并发编程:剖析ThreadLocal

邓竣
 邓竣
发布于 2016/05/12 16:44
字数 2668
阅读 53
收藏 10
点赞 2
评论 0

Java并发编程:深入剖析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;

}

© 著作权归作者所有

共有 人打赏支持
邓竣
粉丝 2
博文 29
码字总数 42960
作品 0
成都
程序员
Java 编程之美:并发编程高级篇之一

本文来自作者 追梦 在 GitChat 上分享 「Java 编程之美:并发编程高级篇之一」 编辑 | 工藤 前言 借用 Java 并发编程实践中的话:编写正确的程序并不容易,而编写正常的并发程序就更难了。 ...

gitchat
05/24
0
0
Spring的事务管理难点剖析(4):多线程的困惑

由于Spring的事务管理器是通过线程相关的ThreadLocal来保存数据访问基础设施(也即Connection实例),再结合IoC和AOP实现高级声明式事务的功能,所以Spring的事务天然地和线程有着千丝万缕的...

icheer
2012/07/17
0
0
读书笔记之《Java并发编程的艺术》-并发编程容器和框架(重要)

读书笔记部分内容来源书出版书,版权归本书作者,如有错误,请指正。 欢迎star、fork,读书笔记系列会同步更新 git https://github.com/xuminwlt/j360-jdk module j360-jdk-thread/me.j360....

Hi徐敏
2015/11/11
0
1
JVM内存结构 VS Java内存模型 VS Java对象模型

Java作为一种面向对象的,跨平台语言,其对象、内存等一直是比较难的知识点。而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚。比如本文我们要讨论的JVM内存结构、Java内存模型和...

Java架构
07/11
0
0
书单丨5本Java后端技术书指引你快速进阶

一名Java开发工程师 不仅要对Java语言及特性有深层次的理解 而且需要掌握与Java相关的 框架、生态及后端开发知识 本文涉及多种后端开发需要掌握的技能 对于帮助提高开发能力非常有帮助 NO.1...

Java高级架构
05/30
0
0
亮剑JVM的9款编程语言杀手开发利器重磅推荐

随着各种各样的编程语言铺地盖地向我们涌来,软件世界似乎变得有点疯狂了。JVM的帝国在不断地壮大,它已经不满足于只作为Java语言的运行平台。它勇敢地将自己的触角伸向了JRuby,Groovy等等,...

ndjys
2012/03/19
0
0
读书笔记之《Java并发编程的艺术》-并发编程基础

读书笔记部分内容来源书出版书,版权归本书作者,如有错误,请指正。 欢迎star、fork,读书笔记系列会同步更新 git https://github.com/xuminwlt/j360-jdk module j360-jdk-thread/me.j360....

Hi徐敏
2015/11/11
0
8
Java程序员必读书单,家族又添新成员

点击关注异步图书,置顶公众号 每天与你分享IT好书 技术干货 职场知识 参与文末话题讨论,每日赠送异步图书。 ——异步小编 有些革命出其不意地吸引了全世界的眼球。Twitter、Linux操作系统和...

异步社区
05/09
0
0
【转】15个顶级Java多线程面试题及回答

Java 线程面试问题   在任何Java面试当中多线程和并发方面的问题都是必不可少的一部分。如果你想获得任何股票投资银行的前台资讯职位,那么你应该准备很多关于多线程的问题。在投资银行业务...

一只死笨死笨的猪
2014/09/30
0
0
15个顶级Java多线程面试题及回答

Java 线程面试问题 在任何Java面试当中多线程和并发方面的问题都是必不可少的一部分。如果你想获得任何股票投资银行的前台资讯职位,那么你应该准备很多关于多线程的问题。在投资银行业务中多...

LCZ777
2014/05/27
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

Activiti - 新一代的开源 BPM 引擎

Activiti 背景简介、服务和功能介绍 董 娜, 狄 浩, 和 张 晓篱 2012 年 7 月 23 日发布 背景介绍 Activiti 其核心是 BPMN 2.0 的流程引擎。BPMN 是目前被各 BPM 厂商广泛接受的 BPM 标准,全...

孟飞阳
10分钟前
0
0
最有效的方式来适配

最有效的方式来进行屏幕适配 在上代码之前先把屏幕相关的几个概念搞清楚:Density、DensityDpi、ScaleDensity。这里我们不过多讲解这些概念知识。 1.0 获取设计图的屏幕尺寸:这里我以360dp...

android-key
12分钟前
0
0
微信授权代码翻译样本

var a,b,c,d = ngx.call(1,2,3)var e = [];var f ;var g = function () {}var h = 1;var c = "abcdefg" + "222";var d = "asdasdasd" + a;var a = ngx >>> log();//......

钟元OSS
14分钟前
0
0
5、二维码生成工具类

一、maven引入依赖jar包 <dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.1.0</version></dependency><dependency><groupId>com.......

丑陋的皮囊
14分钟前
0
0
正则表达式

以前用正则表达式很少,大部分情况下matches一下就好了,这次遇到的情况比较特殊,因为对截取出来的数据比较敏感(日期),所以就重新熟悉了一下,感觉有必要记录一下: /** * 从字符串中...

lost_keke
16分钟前
1
0
Java语言学习(十一):枚举类型和泛型

Java中一个重要的类型:枚举,它可以用来表示一组取值范围固定的变量,使用 enum 关键字定义枚举类型,其中元素不能重复,通常大写表示。利用Java的反射机制,可以在运行时分析类,如查看枚举...

海岸线的曙光
19分钟前
0
0
XGboost调参

参见 :https://blog.csdn.net/u010665216/article/details/78532619?utm_source=debugrun&utm_medium=referral 待补充...

小叮当_加V
26分钟前
0
0
Vue使用问题解决记录(持续更新)

1 this属性调用无效 this在methods中的方法表面调用,表明调用的是当前vue对象. 但在方法内部的方法中调用时,所指便不再是vue对象,可能式窗口本身. 此时建议,在methods中的方法开始时写: var...

社哥
27分钟前
0
0
美国最新超级计算机Summit顶替中国神威超算榜首位置[图]

美国最新超级计算机Summit顶替中国神威超算榜首位置[图]: 2018年6月8日,美国能源部橡树岭国家实验室宣布,制造出了全世界目前最快的超级计算机Summit,顶替了中国“神威太湖之光”在超算排...

原创小博客
30分钟前
1
0
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!

背景 ssh登录的时候,出现的错误: zylMBP:Downloads zhangyalin$ ssh root@192.168.56.108@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ WARNING: REMOTE HOST IDENT......

亚林瓜子
31分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部