背景:
最近项目中需要调用其他业务系统的服务,使用的是Java的RMI机制,但是在调用过程中中间件发生了Token校验问题。而这个问题的根源是每次用户操作,没有去set Token导致的。这个Token是存储在ThreadLocal变量中的,根据servlet的单例多线程原理,使用一个拦截器每次向Thread中写入这个token完美的解决了这个问题。
ThreadLocal
ThreadLocal是Java lang包里面的一个类,这个类用来提供线程级变量的实现。想要明白ThreadLocal首先应该明白Java线程和Java对象的关系。Java对象就是一堆事物(或比作物料),线程就好比工厂流水线。不同流水线之间是独立的,它们可以使用同一堆物料,也可以使用自己的物料,可以说Java现场和对象是两个维度的东西。
我们在Java中经常讨论的线程同步问题就是不同流水线之间共享物料的问题,如果不对共享的对象加以控制,那么线程就会出现各种奇怪的问题,比如原料为0的情况下还有Thread继续生成产品。
在Java线程中比较容易忽视的问题是线程局部变量,或者说那些对象是线程特有的,不同线程之间独立使用,线程结束后这些对象会被gc回收。我们可以看下面的代码:
public class ThreadLoaclTest { public static void main(String[] args) { SequenceNumberRandom r = new SequenceNumberRandom(); Client c1 = new Client(r); Client c2 = new Client(r); Client c3 = new Client(r); Client c4 = new Client(r); c1.start(); c2.start(); c3.start(); c4.start(); } } class SequenceNumberRandom { private int n = 0; private static ThreadLocal<Integer> n = new ThreadLocal<Integer>(){ public Integer initialValue(){ return 0; } }; public int getNextNum() { //int m = n.get() + 1; //n.set(m); //return n.get(); return n++; } } class Client extends Thread { private SequenceNumberRandom r; public Client(SequenceNumberRandom r) { this.r = r; } @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + " : " + r.getNextNum()); } } }
其实上面三种方法殊途同归,指导思想都是为每个线程new一个对象。
上面的程序,我们其实是希望SequenceNumberRandom类的对象能为每个Thread独立的产生连续的序号,就好像每个生产线都独立产生自己的生成序列号,想要实现这个任务。我们当然可以修改上面的程序,构造生成线程的时候独立的去实例化SequenceNumberRandom对象。或者在生成线程内构造SequenceNumberRandom对象。
但是如果我们的需求变了呢?我们想要加一个开关,在某段时间内,一个线程生成的产品ID要一致,不同线程间要不一致。那么这个时候我们会发现,我们其实希望的不是让每个线程都一个自己的对象a,希望独立拥有的只是a里面的一个变量b,并且我们的a如果能是一个单例,那么我们的程序会有更好的扩展性、维护性。
针对这个问题Java为我们提供了ThreadLocal类来实现线程间变量的隔离,我们只需要把对象a的b变量定义为ThreadLocal类型,然后使用get和set方法就可以为每个线程读取值。(注释部分就是ThreadLocal的实现)
用法
ThreadLocal的用法很简单,我们可以把它作为一种“变量类型”来使用,记住它的用途:为每个线程提供一个变量副本。当定义变量的时候用ThreadLocal<T>类代替即可。
当读取变量数据的时候使用get,当设置变量数据的时候用set。当然,既然是变量,那么就应该有初始值,如果我们没有set过变量,那么get的时候会获得null,如果我们不希望初始值是null,可以自己覆盖initalValue方法,通常情况下我们都是下面的这种方式重写initalValue方法:
// private int n = 0; private static ThreadLocal<Integer> n = new ThreadLocal<Integer>(){ public Integer initialValue(){ return 0; } };
原理
说起ThreadLocal原理,牢记我们上面的中心思想,为每个Thread 建立一个对象,通读ThreadLocal源码就会发现,它内部有一个ThreadLocalMap,这个Map用来存储变量,key是ThreadLocal类型,它的一个引用放在了Thread类中。
初始化:
ThreadLocal构造方法并没有做事情,它的成员变量大部分是static的,是用来为ThreadLocal对象生成一个唯一HashCode的。
调用get:
调用get方法时,ThreadLocal会调用Thread.currentThread方法获取当前线程对象,然后读取当下线程对象的ThreadLocalMap对象,如果该Map对象为不null就把this作为key,调用getEntry方法获取线程变量。如果为map或者value为null则调用setInitialValue方法。
调用set:
调用set原理和get一样。
注意:
我们需要注意的是,ThreadLocal只是简单的提供了一种线程变量隔离的方法,对于基本数据类型它能很好工作,如果是对象类型,两个线程可能访问同一个对象。
Synchronized和ThreadLocal
现在网上的资料对Synchronized和ThreadLocal的对比分析文章已经很多了,这里总结下作者们的中心思想。
1、二者性质不同:Synchronized是Java关键字,ThreadLocal是一个Java类。
2、二者功效不同:Synchronized是用来同步线程资源的,ThreadLocal是用来隔离线程资源的。
3、实现机制:Synchronized靠的Java锁机制实现,ThreadLocal靠的是给每个线程建立一个Map实现的,线程销毁则这个Map销毁。
总结:
ThreadLocal是用来进行线程变量隔离的,让每个线程都有一个变量副本。ThreadLocal常出现在中间件编程中。我们可以这样用它:
比如用户登录后,每个用户都有一个LoginContext,但是我们不知道哪个业务组件会需要这个LoginContext。
最简单也是最常用的方式是业务组件自己在接口中明确指出自己的方法需要一个LoginContext,但是如果我们的业务组件发生了变量,比如安全校验机制变了,那么我们是需要修改接口的,这会很麻烦,甚至修改扩展到整个系统。
那么上面的一种解决方案就是,我们为用户的操作加入一个拦截器,然后在拦截器中把用户的LoginContext放入一个单例对象的ThreadLocal变量中,业务组件可以从这个单例对象中读取信息。这样就不需要在接口中明确指明需不需要LoginContext了。
当然上面只是举个例子不是一个实际业务场景。见到过的一个实际场景:
背景中提到的,在SSO登录系统中,用户的每步操作都可能需要校验登录时系统产生的Token,这个时候我们把整改Token扩展到系统中是不合适的,所以把token存在一个单例对象中心,token用ThreadLocal存储。
最后,ThreadLoacl如果写中间件时可能会很好用,但是不建议滥用,有些时候明确的指明需要的东西有利于系统的稳定性。