典型场景1:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)
典型场景2:每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。
假设有一个转换日期的date方法调用SimpleDateFormat类的format方法来实现日期的转换
public String date(int seconds){
Date date=new Date(1000*seconds); //毫秒转换为秒
SimpleDateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
第一步我们要创建十个线程来执行日期转换,代码如下
public class ThreadLocalTest01 {
public static void main(String[] args) {
for(int i=0;i<10;i++){ //创建十个线程
int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
String date=new ThreadLocalTest01().date(finalI); //调用自己的date方法
System.out.println(date);
}
}).start();
}
}
}
代码执行的结果如下:
第二步我们需要创建1000个线程来完成日期转换
当线程数来到1000后再使用for loop创建1000线程就比较耗费资源了,此时我们想到了上一节的线程池
代码如下:
public class ThreadLocalTest02 {
public static ExecutorService threadpool=
Executors.newFixedThreadPool(10); //创建一个固定核心线程数的线程池
public static void main(String[] args) {
for(int i=0;i<1000;i++){
int finalI = i;
threadpool.submit(new Runnable() { //向线程池中提交任务
@Override
public void run() {
String date=new ThreadLocalTest01().date(finalI);
System.out.println(date);
}
});
}
threadpool.shutdown();
}
}
运行结果如下,结果显示并没有什么问题
但此时我们想到这1000个线程要创建1000个SimpleDateFormat类是不是有点浪费资源
我们尝试只用一个SimpleDateFormat类并将其设为静态资源,每个线程直接调用即可
第三步共享SimpleDateFormat类
public class ThreadLocalTest02 {
public static ExecutorService threadpool=
Executors.newFixedThreadPool(10); //创建一个固定核心线程数的线程池
public static SimpleDateFormat dateFormat=
new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");; //将SimpleDateFormat改为静态
public static void main(String[] args) {
for(int i=0;i<1000;i++){
int finalI = i;
threadpool.submit(new Runnable() { //向线程池中提交任务
@Override
public void run() {
Date date1=new Date(1000*finalI); //毫秒转换为秒
String date=dateFormat.format(date1); //直接调用静态成员的方法
System.out.println(date);
}
});
}
threadpool.shutdown();
}
}
此时代码的执行结果如下,此时已经可以看到问题,有两个线程的执行结果相同,但是传入每个线程的时间都是不同的,说明出现了线程安全问题
第四步使用synchronized加锁
public void run() {
Date date1=new Date(1000*finalI); //毫秒转换为秒
String date=null;
synchronized (ThreadLocalTest03.class){ //调用共享资源dateFormat时加锁,可避免线程安全问题
date=dateFormat.format(date1);
}
System.out.println(date);
}
使用这种方法是可行的,加锁之后同一时间只有一个线程调用dateFormat
但是这种方法造成线程都在排队等待释放锁,必然导致效率的降低
第五步使用ThreadLocal完美解决问题
public class ThreadLocalTestFinal {
public static ExecutorService threadpool = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
for(int i=0;i<1000;i++){
int finalI=i;
threadpool.submit(new Runnable() {
@Override
public void run() {
//通过threadLocal的get方法获取到SimpleDateFormat对象
//这是线程独有的自己的对象
String date=TreadLocalTest.threadLocal.get().format(1000*finalI);
System.out.println(date);
}
});
}
threadpool.shutdown();
}
}
class TreadLocalTest{
public static ThreadLocal<SimpleDateFormat> threadLocal=new ThreadLocal<SimpleDateFormat>(){
//需重写initialvalue方法,返回指定格式的SimpleDateFormat
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
}
实例当前用户信息需要被线程所有方法共享,如果每次都将用户信息作为参数进行传递是很麻烦的
让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
在任何方法中都可以轻松获取到该对象
总结ThreadLocal的两个作用就是“线程间隔离,线程内共享”
关于初始化的时机
使用ThreadLocal的好处
–相比于每个任务都新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销
T initialValue( ):初始化并返回当前线程对应的"初始值",这是一个延迟加载的方法,只有在调用get的时候,才会触发,如果此前已经使用了set方法,不会再调用initialValue() 。如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。
void set(T t):为这个线程设置一个新值
T get( ):得到这个线程对应的value。如果是首次调用get(),则会调用initialize来得到这个值
void remove( ):删除对应这个线程的值
首先要理清楚Thread,ThreadLocal,ThreadLocalMap三者关系
protected T initialValue() {
return null;
}
public T get() {
Thread t = Thread.currentThread(); //获取当前线程
ThreadLocalMap map = getMap(t); //获取当前线程的ThreadLocalMap变量
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value; //如果非空,以当前类为key获取value值
return result;
}
}
return setInitialValue(); //如果为空,则执行初始化方法
}
private T setInitialValue() {
T value = initialValue(); //调用默认的初始化方法赋值,如果没有重写则为默认值null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value); //如果存在map,直接set值
else
createMap(t, value); //不存在map,则创建一个map并赋初值
return value;
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
与setInitialValue方法很类似
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread()); //获取到ThreadLocalMap变量
if (m != null)
m.remove(this); //移除以此类为key的entity
}
ThreadLocalMap 类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对:
内存泄漏概念:某个对象不再有用,但是占用的内存却不能被回收
key的泄漏:ThreadLocalMap中的Entry继承自WeakReference,是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
弱引用的特点:如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收所以弱引用不会阻止GC,因此这个弱引用的机制
ThreadLocalMap 的每个 Entry 都是一个对key的弱引用,同时每个 Entry 都包含了一个对value的强引用
正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了
但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链用
因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM
如何避免:调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal之后,应该调用remove方法
如果在set之前调用get,会返回一个null值
因为泛型T的原因,返回的是一个包装的类,如果get得到的是一个基本类型如int,那么在null转换为基本类型时会报空指针异常
ThreadLocal的典型应用场景:每次HTTP请求都对应一个线程,线程之间相互隔离
看RequestContextHolder,也是用到了ThreadLocal,看NamedThreadLocal源码,再看getRequestAttributes的调用
在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄漏