一、概述
ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
二、实现原理
1. 方法
public T get(); //获取ThreadLocal在当前线程中保存的变量副本
public void set(T value); //设置当前线程中变量的副本
public void remove(); //用来移除当前线程中变量的副本
protected T initialValue(); //延迟加载方法,需要时进行重写
2. get方法的实现
public T get() {
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//将ThreadLocal作为key,获取对应的value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
//获取成功,返回value
return result;
}
}
//获取失败,为线程创建ThreadLocalMap,并添加key-value键值对
return setInitialValue();
}
ThreadLocalMap用来存储键值对映射,其中键为WeakReference<ThreadLocal>对象,值为要保存的变量。提供getEntry、set和remove方法用来访问、设置和删除键值对。(数据结构为数组,初始容量16,负载因子2/3,解决冲突的方法是再hash法,即在当前hash的基础上再自增一个常量。)
3. set方法的实现
public void set(T value) {
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//已经存在ThreadLocalMap,直接修改
map.set(this, value);
else
//不存在ThreadLocalMap,先初始化再修改
createMap(t, value);
}
4. remove方法的实现
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
5. 总结
首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,用来存储实际的变量副本,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
初始时,Thread里面threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,会先对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,保存到threadLocals中。
之后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
三、使用方式
class Test{
private static class Person{
private String name;
private int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
public String getName(){
return name;
}
public int getAge(){
return age;
}
public void setAge(int age){
this.age = age;
}
public String toString() {
return "Person@" + Integer.toHexString(hashCode()) + ": {name=" + name + ", age=" + age + "}";
}
}
private static ThreadLocal<Person> mPerson= new ThreadLocal<Person>(){
protected Person initialValue(){
return new Person("Lynn", 20);
}
};
public static void main(String[] args){
ThreadA threadA = new ThreadA();
ThreadB threadB = new ThreadB();
threadA.start();
threadB.start();
}
static class ThreadA extends Thread{
public void run(){
Person person = mPerson.get();
System.out.println("ThreadA:" + person.toString());
person.setAge(18);
System.out.println("ThreadA:" + person.toString());
}
}
static class ThreadB extends Thread{
public void run(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Person person = mPerson.get();
System.out.println("ThreadB:" + person.toString());
person.setAge(20);
System.out.println("ThreadB:" + person.toString());
}
}
}
输出结果:
ThreadA:Person@6e9ef235: {name=Lynn, age=20}
ThreadA:Person@6e9ef235: {name=Lynn, age=18}
ThreadB:Person@6e5070e8: {name=Lynn, age=20}
ThreadB:Person@6e5070e8: {name=Lynn, age=25}
表明各个线程访问到的是不一样的对象。
注意:
要保证各个线程访问到的是不一样的对象,需要保证initialValue方法或者Supplier<? extends S>返回的是不一样的对象。如果返回的是相同的对象引用,会导致各个线程访问到的也是相同的对象。
将之前的创建对象代码改为:
private static Person lynn = new Person("Lynn", 20);
private static ThreadLocal<Person> mPerson = new ThreadLocal<Person>() {
protected Person initialValue() {
return lynn;
}
};
输出结果为:
ThreadA:Person@6d74829c: {name=Lynn, age=20}
ThreadA:Person@6d74829c: {name=Lynn, age=18}
ThreadB:Person@6d74829c: {name=Lynn, age=18}
ThreadB:Person@6d74829c: {name=Lynn, age=25}
获得到的相同的对象。
四、使用场景
使用场景可以归纳成两点:
- 解决对象共享问题。每个线程访问自己的变量副本,保证安全性。
- 作为一种对象保存和访问方式。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
常用场景
1. 安全地访问线程不安全对象
比如使用SimpleDateFormat对象进行日期格式化。由于SimpleDateFormat本身不是线程安全的,因此使用ThreadLocal来给每个线程缓存一个SimpleDateFormat实例,保证线程可以安全的访问SimpleDateFormat。
2. 数据库连接池实现
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
3. session管理
下面这段代码摘自:http://www.iteye.com/topic/103804
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;
}
五、其他
1. 内存泄漏问题
ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。如果一个ThreadLocal没有外部强引用引用它,那么系统gc的时候,这个ThreadLocal会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
无法回收,造成内存泄露。
解决方案:
ThreadLocalMap自身做了优化,在每次get/set时,将访问到的key为null的Entry置为null,擦除对value的引用。
手动调用remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。