1.使用场景

  • 每个线程需要一个独享的对象(通常是工具类,例如SimpleDateFormat在并发情况下是不安全的
  • 每个线程内需要保存全局变量(例如拦截器的用户信息

1.独享对象

每个Thread内有自己的实例副本,不共享

比喻:一个班级有一本教材,学生要做笔记的话会有线程安全,那么就每个学生打印一份

例子:SimpleDateFormat的进化

刚开始我们线程池中有1000个线程任务,它们都需要去调用date方法来进行日期格式转化

此时的date方法中会单独创建SimpleDateFormat对象,那么1000个任务就要创建1000个对象使用完毕后又要进行销毁会造成资源浪费

  1. /**
  2. * 毫秒 转为 yyyy-MM-dd hh:mm:ss
  3. * @param seconds
  4. * @return
  5. */
  6. public String date(int seconds){
  7. Date date = new Date(1000*seconds);
  8. SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
  9. return dateFormat.format(date);
  10. }

于是我们将SimpleDateFormat变成了一个公共对象

ThreadLocal - 图1

然后再运行线程池时,就出现了问题:

ThreadLocal - 图2

有多个值相同的结果出现了!

这是因为多个线程共用一个对象造成的!而SimpleDateFormat本身又不是线程安全的,所以就出现了这种现象!

ThreadLocal - 图3

如何解决?===>上锁!

public String date(int seconds){
    Date date = new Date(1000*seconds);
    String s = null;
    //类锁
    synchronized (ThreadLocalNormalUsage03.class){
        s = dateFormat.format(date);
    }

    return s;
}

但是使用锁就会造成效率降低!

ThreadLocal闪亮登场!

使用ThreadLocal 每个线程会生成一份属于他们自己的format对象,这样就不会有并发问题也不会降低性能!

定义一个类:每个线程进入该方法只会生成一份!

需要重写==initialValue()==方法

class ThreadSafeFormatter{
    //每个线程会生成一份
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
           return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };
}

date方法通过ThreadSafeFormatter来获得对象

public String date(int seconds){
    Date date = new Date(1000*seconds);
    //SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
    return dateFormat.format(date);
}

2.全局变量

例子:不同请求携带着不同的用户信息,那么就不能用static来存放信息了!

升级例子:在上个例子的基础上,使用一个UserMap来存放用户信息,每个线程通过不同的key来get出相应的用户信息

ThreadLocal - 图4

但是!多个线程来操作map就会有线程安全问题,那么就又需要用到 synchronized / ConcurrentHashMap 但是不论用以上哪个,都会对性能造成影响!!!

最好的方法: ThreadLocal

保存当前线程的用户信息

  • 强调的是用一个请求(线程)内不同方法间的共享
  • 不需要重写initialValue(),但是必须手动调用set()
/**
 * 演示ThreadLocal用法2:避免参数传递的麻烦
 */
public class ThreadLocalNormalUsage05 {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        for (int i = 0; i <3; i++) {
            threadPool.execute(()->{
                new Service1().process();
            });
        }
    }
}
//方法1
class Service1{
    public void process(){
        User user = new User("Roderick"+Thread.currentThread().getName());
        //将用户信息set到ThreadLocal
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}
//方法2
class Service2{
    public void process(){
        //省略业务逻辑
        User user = UserContextHolder.holder.get(); //从ThreadLocal中获取当前线程的变量
        System.out.println("Service2获取到user"+user.toString());
        new Service3().process();
    }
}
//方法3
class Service3{
    public void process(){
        //省略业务逻辑
        User user = UserContextHolder.holder.get();//从ThreadLocal中获取当前线程的变量
        System.out.println("Service3获取到user"+user.toString());
    }
}

class UserContextHolder{
    //ThreadLocal
    public static ThreadLocal<User> holder = new ThreadLocal<>();

}

class User{
    String name;

    public User(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }
}

ThreadLocal - 图5

通过ThreadLocal,即使在不同方法中也能获取到当前线程存放的值

3.总结

ThreadLocal的两个作用

  • 每个线程都有自己的独立对象【线程隔离】
  • 可以在任何方法中轻松获取到对象

如何正确选择使用场景(共享对象生成时机)

  • 场景一:initialValue(对象初始化时机由我们控制)
    在初始化ThreadLocal时,就可以将对象初始化,例如(SimpleDateFormat)ThreadLocal - 图6
  • 场景二:set(对象初始化时机不由我们控制
    ThreadLocal里的对象并不是在创建ThreadLocal时就设置好的!而是需要等拦截器传递(如User)后再手动的将其set进去!以便后续使用ThreadLocal - 图7

2.好处

  • 线程安全
  • 不需要加锁,提高效率
  • 高效的利用内存、节省开销

    例如:相比每个任务都新建一个SimpleDateFormat来保证线程安全,显然ThreadLocal可以节省空间

  • 避免传参的麻烦

    可以在任何方法内都获取到线程的ThreadLocal对象,无需通过参数传递

3.原理

  • Thread、ThreadLocal、ThreadLocalMap三者关系

ThreadLocal - 图8

每个Thread对象中都持有一个ThreadLocalMap对象,Map中以KeyValue存储ThreadLocal

4.主要方法

1.initialValue()

初始化 (没有默认实现,需重写)

  • 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get()的时候才会触发
  • 当线程第一次使用get访问变量,才会调用initialValue()
    但是!当之前使用了set方法,那么就不会调用initialValue()
  • 每个线程最多只需要调用一次initialValue().但是!如果调用了remove()那么下次get()会再次调用

2.void set()

为线程设置值

ThreadLocal - 图9

3.get()

得到线程对应的value,如果是首次调用get(),并且没有set值,那么会调用initialize来得到值

主要逻辑:先取出当前线程的ThreadLocalMap然后调用map.getEntry将本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value

ThreadLocal - 图10

map.getEntry传入的是this—->当前ThreadLocal,将其作为key去找到value

4.remove()

删除对应这个线程的值

先获取当前线程的ThreadLocalMap

如何移除Map中的value(以当前ThreadLocal做key)

5.ThreadLocalMap

ThreadLocalMap类,也就是Thread.threadLocals

  • ThreadLocalMap类是每个线程Thread类里的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map:
    • key:当前ThreadLocal
    • value:实际存放的变量,如user、SimpleDateFormat
  • 从源码来看,setInitialValueset()最后都是利用map.set()来设置值到Map中
  • 最后都会对应到ThreadLocalMap的一个Entry

6.注意点

1.内存泄漏

某个对象不再有用,但是占用的内存却不能被回收!

在ThreadLocalMap类中,我们可以看到他的内部类Entry的key是弱引用的,但是value是强引用!

ThreadLocal - 图11

key’是弱引用在gc的时候能够被顺利回收,但是value却是强引用,那么就有可能造成内存泄漏问题

  • 正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收
  • 但是!如果线程不终止【例如线程池!】,那么key对应的value就不能被回收!

    那么当线程多次执行任务。虽然Map中的key是弱引用会被gc, 但是value不会!这就造成了内存泄漏;可能会导致OOM

JDK已经考虑到了这个问题,所以在set remove rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value就会被gc

但是万一我们不调用set remove rehash怎么办呢?

如何避免内存泄漏?(阿里规约)

当我们使用完ThreadLocal,就应该调用remove()将value设为null!!

2.空指针

在ThreadLocal没有set的情况下进行get,一般会得到null

但是需要注意的是,接收的时候避免装箱拆箱,否则可能会导致空指针异常

例如ThreadLocal用long接受

3.共享对象

假设每个线程中ThreadLocal.set()传入的本身就是多线程共享的对象,例如static

那么使用get取出来的还是这个共享对象本身,还是有可能造成并发问题!

较量避免放入这种对象

7.Spring实例

在框架由ThreadLocal相关使用的情况下,优先使用框架的,尽量不要自己维护

例如DateTimeContextHolder类,就使用到了ThreadLocal

每次HTTP请求都对应一个线程,线程之间相互隔离,这也是ThreadLocal的典型应用场景