ThreadLocal详解

第二章 ThreadLocal

1、两大使用场景 

小伙伴们看完 两大使用场景 后或许有些疑惑,请阅读下面的“ 3 、重要方法”内容,可能会对您有所帮助。

1、线程需要一个独享的对象(例如工具类,典型需要使用的类有 SimpleDateFormat 和 Random)。

  1)并发使用静态工具类是有很大风险的,此时可以使用 ThreadLocal 为每个线程都制作一个独享的对象;

  2)使用线程池加上 ThreadLocal 可以节省资源,不需要创建更多的对象浪费资源。

 

示例:

public class ThreadLocalNormal {
    public static ExecutorService service = Executors.newFixedThreadPool(5);

    public String dateFormat(int seconds){
     //每个线程只会触发一次下面的 initialValue() SimpleDateFormat format
= ThreadSafeFormatter.threadLocal.get(); return format.format(new Date(seconds* 1000)); } public static void main(String[] args) { //使用此方法就不需要创建 1000 个 SimpleDateFormat 对象了,有多少个线程,就创建多少个 SimpleDateFormat 对象就够了 for (int i = 0; i < 1000; i++) { int finalI = i; service.submit(() -> { System.out.println(new ThreadLocalNormal().dateFormat(finalI)); }); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } service.shutdown(); } } class ThreadSafeFormatter { public static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(){ @Override //这个函数是初始化作用 protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yy-mm-dd hh:mm:ss"); } }; }

 

 

 

 

2、线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。

  1)例如用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、user ID 等),就不需要把一个信息从 service1 传递到 service2 在传递 service3 .......

  2)每个线程使用 ThreadLocal 保存的内容是不共享的,自己独有的。

 

 示例1:

/**
 * 避免传递参数的麻烦
 */
public class ThreadLocalNormal3 {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 2; i++) {
            int j = i;
            executorService.execute(() -> {
                Service1 service1 = new Service1();
                service1.process("张三"+j,"test"+j);
            });
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        executorService.shutdown();
    }
}
class Service1 {
    public void process(String name,String password){
        User user = new User(name,password);
        UserContextHolder.holder.set(user);
        new Service2();
    }
}

class Service2 {
    public Service2(){
        System.out.println(Thread.currentThread().getName()+"---我是Service2:" + UserContextHolder.holder.get());
        new Service3();
    }
}

class Service3 {
    public Service3(){
        System.out.println(Thread.currentThread().getName()+"---我是Service3:" + UserContextHolder.holder.get());
    }
}
class UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<>();

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

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

 

运行结果:

 

示例二:

public class ThreadLocalNormal3 {
    public static ThreadLocal local = new ThreadLocal();

    public static void main(String[] args) throws InterruptedException {
        local.set("我是主线程");
        new Thread(() -> {
            local.set("我是线程一");
            System.out.println(local.get());
        },"线程一").start();
        Thread.sleep(1000);
        System.out.println(local.get());
    }
}

 

运行结果:

 

2、使用 ThreadLocal 的好处

1、达到线程安全

  1)例如上面的场景一,我们可以把共享的静态对象变成每个线程自己独享的对象。

 

2、不需要加锁,提高执行效率

  1)既然是每个线程自己所独享的资源,线程就是安全的,不需要加锁。

 

3、更高效地利用内存、节省开销

  1)如上面场景一代码以及内容所示。

 

4、免去传参的繁琐:无论是场景一的工具类,还是场景二的用户信息,都可以在任何地方直接通过 ThreadLocal 拿到,再也不需要每次都传递同样的参数。ThreadLocal 使得代码耦合度耕地,更优雅

 

3、重要方法

1、搞清楚 Thread、ThreadLocal、ThreadLocalMap 三者之间的关系:

 

如上图所示:

  1)每个 Thread 对象中持有一个 ThreadLocalMap 成员变量;

  2)TreadLocalMap 是以 ThreadLocal 为 key 以独享的资源为 value;

  3)因为一个 Thread 可以有多个不同的独享资源 ,所以一个 ThreadLocalMap 中是以 ThreadLocal 为 key 来标识每一个资源;

  4)在线程中,可以使用 ThreadLocal.get() 来获取相应的资源。

 

 2、T initialValue() :初始化

  1)该方法会返回当前线程对应的“初始值”(如果没有重写该方法,则初始值为 null),这是一个延迟加载的方法,只有在第一次调用 get 的时候,才会触发(除非.......在  3)叙述.....)

  2)当前线程第一次使用 get 方法时,就会调用此方法,除非线程先前调用了 set 方法,在这种情况下,就不会调用该线程所对应自己的 initialValue 方法;

以上两点正是对应了以上写的两种场景示例代码

  3)通常,每个线程最多调用一次此方法,但如果已经调用了 remove() 后,在调用 get() ,则可以再次调用此方法;

  4)如果不重写本方法,这个方法会返回 null。一般使用匿名内部类的方法来重写 initialValue() ,以便在后续使用中可以初始化副本对象。

 

3、void set(T t) :为这个线程设置一个新值,与setinitialValue() 很类似

源码:

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //如果 map 不为空(该 map 可以存储多个不同的 ThreadLocal),则更改目标 ThreadLocal 所对应的 value(this 为当前的 ThreadLocal)
        //注意:这个 map 以及 map 中的 key 和 value 都是保存在线程中的
        if (map != null)
            map.set(this, value);
            //如果为空,则存入
        else
            createMap(t, value);
    }

 

 

4、T get() :得到 ThreadLocalMap 中对应的 ThreadLocal 的值 。如果是首次调用 get(),则会调用 initialize 来得到这个值

  1)get 方法是先取出当前线程的 ThreadLocalMap,然后调用 map.getEntry 方法,把本 ThreadLocal 的引用作为参数传入,取出 map 中属于本 ThreadLocal 的 value。

源码:

 

5、void remove() :删除 ThreadLocalMap 里面所对应的 key 以及 value(下面代码中,删除所对应的 key 为 local 对象)

  1)remove() 在上面没有做演示,在这里简单的做两个示例:

示例一:

public class ThreadLocalNormal3 {
    public static ThreadLocal local = new ThreadLocal();

    public static void main(String[] args) throws InterruptedException {
        IsNull();
    }
    
    public static void IsNull() throws InterruptedException {
        local.set("我是主线程");
        local.remove();
        Thread.sleep(1000);
        System.out.println("IsNull 方法结果:"+local.get());
    }
}

 

示例二:

public class ThreadLocalNormal3 {
    public static ThreadLocal local = new ThreadLocal();

    public static void main(String[] args) throws InterruptedException {
        IsNull();
    }

    public static void IsNull() throws InterruptedException {
        local.set("我是主线程");
        local.remove();
        local.set("重新 set");
        Thread.sleep(1000);
        System.out.println("IsNull 方法结果:"+local.get());
    }
}

 

源码:

 

6、两种场景殊途同归

  1. setInitialValue 和直接 set 最后都是利用 map.set() 来设置;
  2. 也就是说,最后都会对应到 ThreadLocalMap 的一个 Entry,只不过是起点和入口不一样

 

7、演示:一个线程,一个 ThreadLocalMap,两个 ThreadLocal

public class ThreadLocalDemo {
    //一个主线程
    public static void main(String[] args) throws InterruptedException {
        //设置两个ThreadLocal
        ThreadLocal local1 = new ThreadLocal();
        ThreadLocal local2 = new ThreadLocal();

        //分别为两个 ThreadLocal 设置 value
        local1.set("我是 local1");
        local2.set("我是 local2");

        Thread.sleep(1000);

        //底层会自动的将这两个 ThreadLocal 以及相应的 value 放入主线程的 ThreadLocalMap 中
        //输出 ThreadLocalMap 中的这两个 ThreadLocal 对应的 value
        System.out.println(local1.get()); //我是 local1
        System.out.println(local2.get()); //我是 local2
    }
}

 

4、ThreadLocalMap 类

1、ThreadLocalMap 类是每个线程 Thread 类里面的变量,里面最重要的是一个键值对数组 Entry[] table。可以认为是一个 Map,键值对:

  1. 键:这个 ThreadLocal;
  2. 值:实际需要的成员变量,比如 user 或者 simpleDateFormat 对象。

2、ThreadLocalMap 这里采用的是线性探测法,也就是说,如果发生冲突,就继续找下一个空位置,而不是用链表拉链

 

5、其他

1、注意空指针异常,原理如代码中的注释:

  在进行 get 之前,必须先 set,否则可能报空指针异常,以下代码报空指针异常

public class ThreadLocalNPE {
    ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
    public void set() {
        longThreadLocal.set(Thread.currentThread().getId());
    }
    //上面定义的是 ThreadLocal<Long>,这里如果返回值是 long,那么 get 到的值要做拆箱,如果拿 null 去拆箱,会报空指针异常
    public long get(){
        return longThreadLocal.get();
    }

    public static void main(String[] args) {
        ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
            System.out.println(threadLocalNPE.get());
    }
}

 

 

2、共享对象问题

  1)如果在每个线程中 ThreadLocal.set() 进去的东西本来就是多线程共享的同一个对象,比如 static 对象,那么多个线程的 ThreadLocal.get() 取得的还是这个共享对象本身,还是有并发访问问题;

  2)如果可以不适用 ThreadLocal 就解决问题,那么不要强行使用;

    • 例如在任务数很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到 ThreadLocal。

  3)优先使用框架的支持,而不是自己创造。

    • 例如在 Spring 中,如果可以使用 RequestContextHolder,那么就不需要自己维护 ThreadLocal,因为自己可能会忘记调用 remove() 等,造成内存泄漏。

 

posted @ 2021-09-15 19:43  nicechen  阅读(91)  评论(0编辑  收藏  举报