java ThreadLocal(应用场景及使用方式及原理)

尽管ThreadLocal与并发问题相关,可是很多程序猿只将它作为一种用于“方便传參”的工具,胖哥觉得这或许并非ThreadLocal设计的目的,它本身是为线程安全和某些特定场景的问题而设计的。

ThreadLocal是什么呢。

每一个ThreadLocal能够放一个线程级别的变量,可是它本身能够被多个线程共享使用,并且又能够达到线程安全的目的,且绝对线程安全。

比如:

public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();

RESOURCE代表一个能够存放String类型的ThreadLocal对象。此时不论什么一个线程能够并发訪问这个变量,对它进行写入、读取操作,都是线程安全的。比方一个线程通过RESOURCE.set(“aaaa”);将数据写入ThreadLocal中,在不论什么一个地方,都能够通过RESOURCE.get();将值获取出来。

可是它也并不完美,有很多缺陷,就像大家依赖于它来做參数传递一样。接下来我们就来分析它的一些不好的地方。

为什么有些时候会将ThreadLocal作为方便传递參数的方式呢?比如当很多方法相互调用时,最初的设计可能没有想太多,有多少个參数就传递多少个变量,那么整个參数传递的过程就是零散的。进一步思考:若A方法调用B方法传递了8个參数。B方法接下来调用C方法->D方法->E方法->F方法等仅仅须要5个參数,此时在设计API时就涉及5个參数的入口。这些方法在业务发展的过程中被很多地方所复用。

某一天。我们发现F方法须要加一个參数,这个參数在A方法的入口參数中有,此时,假设要改中间方法牵涉面会非常大。并且不知道改动后会不会有Bug。

作为程序猿的我们可能会随性一想,ThreadLocal反正是全局的,就放这里吧。确实好解决。

可是此时你会发现系统中这样的方式有点像在贴补丁。越贴越多,我们必需要求调用相关的代码都使用ThreadLocal传递这个參数,有可能会搞得乱七八糟的。换句话说,并非不让用。而是我们要明白它的入口和出口是可控的。

诡异的ThreadLocal最难琢磨的是“作用域”,尤其是在代码设计之初非常乱的情况下,假设再添加很多ThreadLocal。系统就会逐渐变成神龙见首不见尾的情况。有了这样一个省事的东西。可能很多小伙伴更加不在意设计,由于大家都觉得这些问题都能够通过变化的手段来解决。胖哥觉得这是一种恶性循环。

对于这类业务场景。应当提前有所准备。须要粗粒度化业务模型。即使要用ThreadLocal,也不是加一个參数就加一个ThreadLocal变量。比如,我们能够设计几种对象来封装入口參数,在接口设计时入口參数都以对象为基础。

或许一个类无法表达全部的參数意思,并且那样easy导致强耦合。

通常我们依照业务模型分解为几大类型对象作为它们的參数包装,而且将依照对象属性共享情况进行抽象,在继承关系的每个层次各自扩展对应的參数,或者说加參数就在对象中加,共享參数就在父类中定义,这种參数就逐步规范化了。

我们回到正题,探讨一下ThreadLocal究竟是用来做什么的?为此我们探讨下文中的几个话题。

(1)应用场景及使用方式

为了说明ThreadLocal的应用场景。我们来看一个框架的样例。Spring的事务管理器通过AOP切入业务代码,在进入业务代码前,会依据相应的事务管理器提取出相应的事务对象,假如事务管理器是DataSourceTransactionManager,就会从DataSource中获取一个连接对象,通过一定的包装后将其保存在ThreadLocal中。而且Spring也将DataSource进行了包装,重写了当中的getConnection()方法,或者说该方法的返回将由Spring来控制,这样Spring就能让线程内多次获取到的Connection对象是同一个。

为什么要放在ThreadLocal里面呢?由于Spring在AOP后并不能向应用程序传递參数。应用程序的每一个业务代码是事先定义好的,Spring并不会要求在业务代码的入口參数中必须编写Connection的入口參数。此时Spring选择了ThreadLocal,通过它保证连接对象始终在线程内部,不论什么时候都能拿到,此时Spring很清楚什么时候回收这个连接,也就是很清楚什么时候从ThreadLocal中删除这个元素(在9.2节中会具体解说)。

从Spring事务管理器的设计上能够看出。Spring利用ThreadLocal得到了一个非常完美的设计思路,同一时候它在设计时也十分清楚ThreadLocal中元素应该在什么时候删除。由此,我们简单地觉得ThreadLocal尽量使用在一个全局的设计上。而不是一种打补丁的间接方法。

了解了基本应用场景后,接下来看一个样例。定义一个类用于存放静态的ThreadLocal对象,通过多个线程并行地对ThreadLocal对象进行set、get操作,并将值进行打印。来看看每一个线程自己设置进去的值和取出来的值是否是一样的。

代码例如以下:

代码清单5-8 简单的ThreadLocal样例

public class ThreadLocalTest {

	static class ResourceClass {

		public final static ThreadLocal<String> RESOURCE_1 =
									   new ThreadLocal<String>();

		public final static ThreadLocal<String> RESOURCE_2 =
									   new ThreadLocal<String>();

	}

	static class A {

		public void setOne(String value) {
			ResourceClass.RESOURCE_1.set(value);
		}

		public void setTwo(String value) {
			ResourceClass.RESOURCE_2.set(value);
		}
	}

	static class B {
		public void display() {
			System.out.println(ResourceClass.RESOURCE_1.get()
						+ ":" + ResourceClass.RESOURCE_2.get());
		}
	}

	public static void main(String []args) {
		final A a = new A();
		final B b = new B();
		for(int i = 0 ; i < 15 ; i ++) {
			final String resouce1 = "线程-" + I;
			final String resouce2 = " value = (" + i + ")";
			new Thread() {
				public void run() {
				try {
					a.setOne(resouce1);
					a.setTwo(resouce2);
					b.display();
				}finally {
					ResourceClass.RESOURCE_1.remove();
					ResourceClass.RESOURCE_2.remove();
				}
			}
		}.start();
		}
	}
}

关于这段代码,我们先说几点。

◎ 定义了两个ThreadLocal变量。终于的目的就是要看最后两个值能否相应上。这样才有机会证明ThreadLocal所保存的数据可能是线程私有的。

◎ 使用两个内部类仅仅是为了使測试简单,方便大家直观理解,大家也能够将这个样例的代码拆分到多个类中,得到的结果是同样的。

◎ 測试代码更像是为了方便传递參数。由于它确实传递參数非常方便,但这不过为了測试。

◎ 在finally里面有remove()操作,是为了清空数据而使用的。

为何要清空数据,在后文中会继续介绍细节。

測试结果例如以下:

线程-6: value = (6)

线程-9: value = (9)

线程-0: value = (0)

线程-10: value = (10)

线程-12: value = (12)

线程-14: value = (14)

线程-11: value = (11)

线程-3: value = (3)

线程-5: value = (5)

线程-13: value = (13)

线程-2: value = (2)

线程-4: value = (4)

线程-8: value = (8)

线程-7: value = (7)

线程-1: value = (1)

大家能够看到输出的线程顺序并不是最初定义线程的顺序,理论上能够说明多线程应当是并发运行的,可是依旧能够保持每一个线程里面的值是相应的,说明这些值已经达到了线程私有的目的。

不是说共享变量无法做到线程私有吗?它又是怎样做到线程私有的呢?这就须要我们知道一点点原理上的东西。否则用起来也没那么放心,请看以下的介绍。

(2)ThreadLocal内在原理

从前面的操作能够发现,ThreadLocal最常见的操作就是set、get、remove三个动作。以下来看看这三个动作究竟做了什么事情。首先看set操作,源代码片段如图5-5所看到的。

图5-5 ThreadLcoal.set源代码片段

图5-5中的第一条代码取出了当前线程t。然后调用getMap(t)方法时传入了当前线程,换句话说。该方法返回的ThreadLocalMap和当前线程有点关系,我们先记录下来。

进一步判定假设这个map不为空。那么设置到Map中的Key就是this。值就是外部传入的參数。这个this是什么呢?就是定义的ThreadLocal对象。

代码中有两条路径须要追踪,各自是getMap(Thread)和createMap(Thread , T)。首先来看看getMap(t)操作。如图5-6所看到的。

图5-6 getMap(Thread)操作

在这里。我们看到ThreadLocalMap事实上就是线程里面的一个属性,它在Thread类中的定义是:

ThreadLocal.ThreadLocalMap threadLocals = null;

这样的方法非常easy让人混淆,由于这个ThreadLocalMap是ThreadLocal里面的内部类。放在了Thread类里面作为一个属性而存在。ThreadLocal本身成为这个Map里面存放的Key,用户输入的值是Value。

太乱了。理不清楚了,画个图来看看(见图5-7)。

简单来讲,就是这个Map对象在Thread里面作为私有的变量而存在,所以是线程安全的。ThreadLocal通过Thread.currentThread()获取当前的线程就能得到这个Map对象。同一时候将自身作为Key发起写入和读取,因为将自身作为Key,所以一个ThreadLocal对象就能存放一个线程中相应的Java对象。通过get也自然能找到这个对象。

图5-7 Thread与ThreadLocal的伪代码关联关系

假设还没有理解,则能够将思维放宽一点。

当定义变量String a时,这个“a”事实上仅仅是一个名称(在第3章中已经说到了常量池),虚拟机须要通过符号表来找到对应的信息,而这样的方式正好就像一种K-V结构,底层的处理方式也确实非常接近这样。这里的处理方式是显式地使用Map来存放数据,这也是一种实现手段的变通。

如今有了思路。继续回到上面的话题,为了验证前面的判断和理解,来看看createMap方法的细节,如图5-8所看到的。

图5-8 createMap操作

这段代码是运行一个创建新的Map的操作。而且将第一个值作为这个Map的初始化值,因为这个Map是线程私有的。不可能有还有一个线程同一时候也在对它做put操作,因此这里的赋值和初始化是绝对线程安全的,也同一时候保证了每个外部写入的值都将写入到Map对象中。

最后来看看get()、remove()代码,也许看到这里就能够认定我们的理论是正确的。如图5-9所看到的。

图5-9 get()/remove()方法的代码片段

给我们的感觉是,这样实现是一种技巧,而不是一种技术。

事实上是技巧还是技术全然是从某种角度来看的。或者说是从某种抽象层次来看的,假设这段代码在C++中实现,难道就叫技术,不是技巧了吗?当然不是。胖哥觉得技术依旧是建立在思想和方法基础上的,仅仅是看实现的抽象层次在什么级别。就像在本书中多个地方探讨的一些基础原理一样,我们探讨了它的思想,事实上它的实现也是基于某种技巧和手段的,仅仅是对程序封装后就变成了某种语法和API,因此胖哥觉得,一旦学会使用技巧思考问题,就学会了通过技巧去看待技术本身。我们应当通过这样的设计,学会一种变通和发散的思维。学会理解各种各样的场景。这样便能够积累很多真正的財富,这些財富不是通过某些工具的使用或測试就能够获得的。

ThreadLocal的这样的设计非常完美吗?

不是非常完美,它依旧有很多坑,在这里对它easy误导程序猿当成传參工具就不再多提了。以下我们来看看它的使用不当会导致什么技术上的问题。

(3)ThreadLocal的坑

通过上面的分析。我们能够认识到ThreadLocal事实上是与线程绑定的一个变量,如此就会出现一个问题:假设没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。

因此,ThreadLocal的一个非常大的“坑”就是当使用不当时,导致使用者不知道它的作用域范围。

大家可能觉得线程结束后ThreadLocal应该就回收了。假设线程真的注销了确实是这种,可是事实有可能并不是如此。比如在线程池中对线程管理都是採用线程复用的方法(Web容器通常也会採用线程池)。在线程池中线程非常难结束甚至于永远不会结束。这将意味着线程持续的时间将不可预測,甚至与JVM的生命周期一致。

那么对应的ThreadLocal变量的生命周期也将不可预測。

或许系统中定义少量几个ThreadLocal变量也无所谓。由于每次set数据时是用ThreadLocal本身作为Key的,同样的Key肯定会替换原来的数据。原来的数据就能够被释放了,理论上不会导致什么问题。但世事无绝对,假设ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会開始膨胀。

抛开代码本身的问题。举一个极端的样例。假设不想定义太多的ThreadLocal变量,就用一个HashMap来存放,这貌似没什么问题。由于ThreadLocal在程序的不论什么一个地方都能够用得到,在某些设计不当的代码中非常难知道这个HashMap写入的源头,在代码中为了保险起见。一般会先检查这个HashMap是否存在,若不存在,则创建一个HashMap写进去。若存在,通常也不会替换掉。由于代码编写者一般会“害怕”由于这样的替换会丢掉一些来自“其它地方写入HashMap的数据”。从而导致很多不可预见的问题。

在这种情况下。HashMap第一次放入ThreadLocal中或许就一直不会被释放,而这个HashMap中可能開始存放很多Key-Value信息,假设业务上存放的Key值在不断变化(比如,将业务的ID作为Key),那么这个HashMap就開始不断变长,并且非常可能在每一个线程中都有一个这种HashMap,逐渐地形成了间接的内存泄漏。以前有非常多人吃过这个亏,并且吃亏的时候发现这种代码可能不是在自己的业务系统中。而是出如今某些二方包、三方包中(开源并不保证没有问题)。

要处理这样的问题非常复杂,只是首先要保证自己编写的代码是没问题的。要保证没问题不是说我们不去用ThreadLocal。甚至不去学习它。由于它肯定有其应用价值。在使用时要明确ThreadLocal最难以捉摸的是“不知道哪里是源头”(一般是代码设计不当导致的),仅仅有知道了源头才干控制结束的部分。或者说我们从设计的角度要让ThreadLocal的set、remove有始有终,通常在外部调用的代码中使用finally来remove数据,仅仅要我们细致思考和抽象是能够达到这个目的的。有些是二方包、三方包的问题,对于这些问题我们须要学会的是找到问题的根源后解决,关于二方包、三方包的执行跟踪,可參看第3.7.9节介绍的BTrace工具。

补充:在不论什么异步程序中(包含异步I/O、非堵塞I/O),ThreadLocal的參数传递是不靠谱的,由于线程将请求发送后。就不再等待远程返回结果继续向下运行了,真正的返回结果得到后,处理的线程可能是还有一个。

#####################################个人总结 ####################################

Thread.java源代码中:

ThreadLocal.ThreadLocalMap threadLocals = null;

即:每一个Thread对象都有一个ThreadLocal.ThreadLocalMap成员变量,ThreadLocal.ThreadLocalMap是一个ThreadLocal类的静态内部类(例如以下所看到的),所以Thread类能够进行引用.

static class ThreadLocalMap {

所以每一个线程都会有一个ThreadLocal.ThreadLocalMap对象的引用

当在ThreadLocal中进行设值的时候:

    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;
    }

首先获取当前线程的引用,然后获取当前线程的ThreadLocal.ThreadLocalMap对象(t.threadLocals变量就是ThreadLocal.ThreadLocalMap的变量),假设该对象为空就创建一个,例如以下所看到的:

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

这个this变量就是ThreadLocal的引用,对于同一个ThreadLocal对象每一个线程都是同样的,可是每一个线程各自有一个ThreadLocal.ThreadLocalMap对象保存着各自ThreadLocal引用为key的值,所以互不影响,并且:假设你新建一个ThreadLocal的对象,这个对象还是保存在每一个线程同一个ThreadLocal.ThreadLocalMap对象之中,由于一个线程仅仅有一个ThreadLocal.ThreadLocalMap对象,这个对象是在第一个ThreadLocal第一次设值的时候进行创建,如上所述的createMap方法.

        ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

总结:

深入研究java.lang.ThreadLocal类:http://blog.csdn.net/xiaohulunb/article/details/19603611

API说明:

ThreadLocal(),T get(),protected T initialValue(),void remove(),void set(T value)

典型实例:

1.Hiberante的Session 工具类HibernateUtil

2.通过不同的线程对象设置Bean属性,保证各个线程Bean对象的独立性。

ThreadLocal使用的一般步骤:

1、在多线程的类(如ThreadDemo类)中。创建一个ThreadLocal对象threadXxx,用来保存线程间须要隔离处理的对象xxx。
2、在ThreadDemo类中。创建一个获取要隔离訪问的数据的方法getXxx(),在方法中推断,若ThreadLocal对象为null时候,应该new()一个隔离訪问类型的对象,并强制转换为要应用的类型。
3、在ThreadDemo类的run()方法中。通过getXxx()方法获取要操作的数据。这样能够保证每一个线程相应一个数据对象,在不论什么时刻都操作的是这个对象。

与Synchonized的对照:

ThreadLocal和Synchonized都用于解决多线程并发訪问。可是ThreadLocal与synchronized有本质的差别。synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程訪问。而ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间訪问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时可以获得数据共享。

Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

一句话理解ThreadLocal:向ThreadLocal里面存东西就是向它里面的Map存东西的,然后ThreadLocal把这个Map挂到当前的线程底下,这样Map就仅仅属于这个线程了。

使用ThreadLocal改进你的层次的划分(spring事务的实现):http://blog.csdn.net/zhouyong0/article/details/7761835

源代码剖析之ThreadLocal:http://wangxinchun.iteye.com/blog/1884228

Java中的ThreadLocal源代码解析(上):http://maosidiaoxian.iteye.com/blog/1939142

ThreadLocal与synchronized:http://blog.csdn.net/yangairong1984/article/details/2294572

Java线程:深入ThreadLocal:http://lavasoft.blog.51cto.com/62575/258459(一个ThreadLocal的模拟实现)

Java多线程(六)、ThreadLocal类:http://blog.csdn.net/lonelyroamer/article/details/7998137

时间: 2024-10-29 19:06:01

java ThreadLocal(应用场景及使用方式及原理)的相关文章

Java ThreadLocal

背景: 最近项目中需要调用其他业务系统的服务,使用的是Java的RMI机制,但是在调用过程中中间件发生了Token校验问题.而这个问题的根源是每次用户操作,没有去set Token导致的.这个Token是存储在ThreadLocal变量中的,根据servlet的单例多线程原理,使用一个拦截器每次向Thread中写入这个token完美的解决了这个问题. ThreadLocal ThreadLocal是Java lang包里面的一个类,这个类用来提供线程级变量的实现.想要明白ThreadLocal首

JAVA性能优化的五种方式

一,JAVA性能优化之设计优化 设计优化处于性能优化手段的上层.它往往须要在软件开发之前进行.在软件开发之前,系统架构师应该就评估系统可能存在的各种潜在问题和技术难点,并给出合理的设计方案,因为软件设计和系统架构对软件总体设计质量有决定性的影响.所以,设计调优对系统的性能影响也是最大的,假设说,代码优化.JVM优化都是对系统微观层次的"量"的优化,那设计优化就是对系统"质"的优化. 设计优化的一大显著特征是:它能够规避某一个组件的性能问题,而是改良组件的实现;比方:

深入研究JAVA ThreadLocal类

深入研究java.lang.ThreadLocal类 一.概述 ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是 threadlocalvariable(线程局部变量).也许把它命名为ThreadLocalVar更加合适.线程局部变量 (ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程

Java ThreadLocal 简介

ThreadLocal在Spring中发挥着重要的作用,在管理request作用域的Bean.事务管理.任务调度.AOP等模块都出现了它们的身影,起着举足轻重的作用.要想了解Spring事务管理的底层技术,ThreadLocal是必须攻克的山头堡垒. 我们知道spring通过各种模板类降低了开发者使用各种数据持久技术的难度.这些模板类都是线程安全的,也就是说,多个DAO可以复用同一个模板实例而不会发生冲突.我们使用模板类访问底层数据,根据持久化技术的不同,模板类需要绑定数据连接或会话的资源.但这

java单例设计模式八种方式

单例设计模式介绍 所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法). 比如Hibernate的SessionFactory,它充当数据存储源的代理,并负责创建Session对象.SessionFactory并不是轻量级的,一般情况下,一个项目通常只需要一个SessionFactory就够,这是就会使用到单例模式. 单例设计模式八种方式 单例模式有八种方式: 饿汉式( ( 静态常 量) ) 饿汉式(静态

Java中创建对象的5种方式 &amp;&amp;new关键字和newInstance()方法的区别

转载:http://www.kuqin.com/shuoit/20160719/352659.html 用最简单的描述来区分new关键字和newInstance()方法的区别:newInstance: 弱类型.低效率.只能调用无参构造.new: 强类型.相对高效.能调用任何public构造. newInstance( )是一个方法,而new是一个关键字,其次,Class下的newInstance()的使用有局限,因为它生成对象只能调用无参的构造函数,而使用new关键字生成对象没有这个限制.Cla

java 定时器的几种实现方式以及 配置参数的说明

2.java中常见的定时器 1)借助Java.util.Timer来实现 2)OpenSymphony社区提供的Quartz来实现 3.介绍Timer 利用Timer开发定时任务是主要分为两个步骤: 1)创建定时任务类 示例代码: package org.lzstone.action import java.util.TimeTask public class LzstoneTimeTask extends TimeTask{ public void run(){ //执行的定时器任务 } }

【Java技术点滴】——XML解析方式比较

为什么? "它可以用来标记数据.定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言.它非常适合万维网传输,提供统一的方法来描述和交换独立于应用程序或供应商的结构化数据.是Internet环境中跨平台的.依赖于内容的技术,也是当今处理分布式结构信息的有效工具."XML被设计用来传输和存储数据: 他的平台无关性.语言无关性.系统无关性,给数据继承与交互带来了极大的方便.也因此使用XML的地方越来越常见. 方式 1.DOM--DocumentObject Model 像操作HTML

java创建线程的三种方式及其对比

Java中创建线程主要有三种方式: 一.继承Thread类创建线程类 (1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务.因此把run()方法称为执行体. (2)创建Thread子类的实例,即创建了线程对象. (3)调用线程对象的start()方法来启动该线程. package com.thread; public class FirstThreadTest extends Thread{ int i = 0; //重写run方法,run方法的方