1. ThreadLocal是用来解决什么问题?
ThreadLocal 是线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问, 通常是类中的 private static 字段,是对该字段初始值的一个拷贝,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
我们知道有时候一个对象的变量会被多个线程所访问,这时就会有线程安全问题,当然我们可以使用synchorinized 关键字来为此变量加锁,进行同步处理,从而限制只能有一个线程来使用此变量,但是加锁会大大影响程序执行效率,此外我们还可以使用ThreadLocal来解决对某一个变量的访问冲突问题。
2. 实现原理
当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。
但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。
3. ThreadLocal接口方法
ThreadLocal 可以存储任何类型的变量对象, get返回的是一个Object对象,但是我们可以通过泛型来制定存储对象的类型。
public T get() { } // 用来获取ThreadLocal在当前线程中保存的变量副本
public void set(T value) { } //set()用来设置当前线程中变量的副本
public void remove() { } //remove()用来移除当前线程中变量的副本
protected T initialValue() { } //initialValue()是一个protected方法,一般是用来在使用时进行重写的
Thread 在内部是通过ThreadLocalMap来维护ThreadLocal变量表, ThreadLocalMap是ThreadLocal类的一个内部类,这个Map里面的最小的存储单位是一个Entry, 它使用ThreadLocal作为key, 变量作为 value,这是因为在每一个线程里面,可能存在着多个ThreadLocal变。在Thread类中声明了一个ThreadLocalMap类型的变量threadLocals,就是它为每一个线程来存储自身的ThreadLocal变量的。
初始时threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
下面通过一个例子来证明通过ThreadLocal能达到在每个线程中创建变量副本的效果:
public class ThreadLocalTest {
public static void main(String[] args) throws InterruptedException {
final ThreadLocalTest test = new ThreadLocalTest();
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
// 在这里新建了一个线程
Thread thread1 = new Thread() {
public void run() {
test.set();
/*当这里调用了set方法,进一步调用了ThreadLocal的set方法是,
会将ThreadLocal变量存储到该线程的ThreadLocalMap类型的成员变量threadLocals
中,注意的是这个threadLocals变量是Thread线程的一个变量,而不是ThreadLocal类的变量。*/
System.out.println(test.getLong());
System.out.println(test.getString());
};
};
thread1.start();
thread1.join();
System.out.println(test.getLong());
System.out.println(test.getString());
}
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();
}
}
代码的输出结果:
1*
main*
9*
Thread-0*
1*
main
这段代码的输出结果可以看出,在main线程中和thread1线程中,longLocal保存的副本值和stringLocal保存的副本值都不一样。最后一次在main线程再次打印副本值是为了证明在main线程中和thread1线程中的副本值确实是不同的,总结如下:
1、 实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
2、 为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;
3、 在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法;
4. 实例分析
先看下面这段关于日期的代码:
package test;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
private static final SimpleDateFormat sdf = new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss");
public static class ParseDate implements Runnable {
int i = 0;
public ParseDate(int i) {
this.i = i;
}
public void run() {
try {
Date t = sdf.parse("2016-02-16 17:00:" + i % 60);
System.out.println(i + ":" + t);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
es.execute(new ParseDate(i));
}
}
}
由于SimpleDateFormat并不线程安全的,所以上述代码是错误的使用。最简单的方式就是,自己定义一个类去用synchronized包装(类似于Collections.synchronizedMap)。这样做在高并发时会有问题,对 synchronized的争用导致每一次只能进去一个线程,并发量很低。这里使用ThreadLocal去封装SimpleDateFormat就可以解决这个问题:
package test;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();
public static class ParseDate implements Runnable {
int i = 0;
public ParseDate(int i) {
this.i = i;
}
public void run() {
try {
if (tl.get() == null) {
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
Date t = tl.get().parse("2016-02-16 17:00:" + i % 60);
System.out.println(i + ":" + t);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
es.execute(new ParseDate(i));
}
}
}
每个线程在运行时,会判断是否当前线程有SimpleDateFormat对象
if (tl.get() == null)
如果没有的话,就new个 SimpleDateFormat与当前线程绑定
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
然后用当前线程的 SimpleDateFormat去解析
tl.get().parse("2016-02-16 17:00:" + i % 60);
一开始的代码中,只有一个 SimpleDateFormat,使用了 ThreadLocal,为每一个线程都new了一个SimpleDateFormat。
需要注意的两个问题:
1、 不要把公共的一个SimpleDateFormat设置给每一个ThreadLocal,这样是没用的需要给每一个都new一个SimpleDateFormat,在hibernate中,对ThreadLocal有典型的应用;
2、 ThreadLocalMap实现和HashMap差不多,但是在hash冲突的处理上有区别ThreadLocalMap中发生hash冲突时,不是像HashMap这样用链表来解决冲突,而是是将索引++,放到下一个索引处来解决冲突;
5. ThreadLocal的应用场景
最常见的ThreadLocal使用场景是用来解决数据库连接、Session管理等。
数据库连接:
Class A implements Runnable{
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
}
Session管理:
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;
}