你了解ThreadLocal吗?

她不清楚本身孤独的缘由
惟一可以表达出来的就是
这不是我所指望的世界。java

介绍

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,通常使用者在访问共享变量的时候须要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式以外的一种保证一种规避多线程访问出现线程不安全的方法,当咱们在建立一个变量后,若是每一个线程对其进行访问的时候访问的都是线程本身的变量这样就不会存在线程不安全问题。git

ThreadLocal是JDK包提供的,它提供线程本地变量,若是建立一乐ThreadLocal变量,那么访问这个变量的每一个线程都会有这个变量的一个副本,在实际多线程操做的时候,操做的是本身本地内存中的变量,从而规避了线程安全问题。
 
threadlocal是一个线程内部的存储类,能够在指定线程内存储数据,数据存储之后,只有指定线程能够获得存储数据。github

在这里插入图片描述

解决问题

问题1

每一个线程须要一个独享的对象(一般是工具类,典型须要使用的类有SimpleDateFormat和Random)。web

你想象的代码面试

public class TestThreadLocal {

    public static void main(String[] args) {
        //多个线程分别调用date方法
        ......
    }

    public static String date(int second) {
        Date date = new Date(second);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return simpleDateFormat.format(date);
    }
}

这个代码最大的问题是什么?
每一个线程分别调用date方法,方法内部须要建立一个SimpleDateFormat对象,那线程若是不少的话,就会建立不少个SimpleDateFormat对象,可能会形成OOM。数组

为何不用一个共享变量SimpleDateFormat达到复用的目的,由于SimpleDateFormat线程不安全。安全

为何不加锁?效率过低。微信

使用ThreadLocal多线程

public class TestThreadLocal {

    public static void main(String[] args) {
        //多个线程分别调用date方法
        ......

    }

    public static String date(int second) {
        Date date = new Date(second);
        //get方法获取ThreadLocal设置的初始值。
        SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.threadLocal.get();
        return simpleDateFormat.format(date);
    }
}

class ThreadSafeFormatter {
	//使用ThreadLocal
    static ThreadLocal<SimpleDateFormat> threadLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}

问题2

每一个线程内须要保存全局变量(避免在拦截器中获取用户信息),可让不一样方法直接使用,避免参数传递的麻烦并发

你想象的代码

public class TestThreadLocal {
    public static void main(String[] args) {

        //每一个线程有本身的用户信息
        User user=new User("Jeck");
        //执行操做
        new Service1().process(user);

    }
}
//模块1
class Service1{
    public void process(User user){
        //执行完操做,调用下一个模块进行操做
        new Service2().process(user);
    }
}
//模块2
class Service2{
    public void process(User user){
        //执行完操做,调用下一个模块进行操做
        new Service3().process(user);
    }
}
//模块3
class Service3{
    public void process(User user){
        //流程结束,删除user信息
    }
}

这段代码最大的弊端是什么?
参数传递的麻烦。

使用ThreadLocal

public class TestThreadLocal {
    public static void main(String[] args) {

        //每一个线程有本身的用户信息
        User user=new User("Jeck");
        //保存到ThreadLocal中
        ThreadSafeUser.threadLocal.set(user);
        //执行操做
        new Service1().process();
    }
}
//模块1
class Service1{
    public void process(){
        //从ThreadLocal中获取User用户信息
        User user=ThreadSafeUser.threadLocal.get();
        //执行完操做,调用下一个模块进行操做
        new Service2().process();
    }
}
//模块2
class Service2{
    public void process(){
        //从ThreadLocal中获取User用户信息
        User user=ThreadSafeUser.threadLocal.get();
        //执行完操做,调用下一个模块进行操做
        new Service3().process();
    }
}
//模块3
class Service3{
    public void process(){
        //流程结束,删除user信息
        ThreadSafeUser.threadLocal.remove();
    }
}
class ThreadSafeUser {
    static ThreadLocal<User> threadLocal =new ThreadLocal<User>();
}

以上两种场景,生成对象时机不一样。

根据共享对象的生成时机不一样,选择initialValue或set来保存对象,后面会介绍几种方法。

好处

  1. 线程安全
  2. 不须要加锁,提升执行效率
  3. 更高效的利用内存,节省开销
  4. 避免传参麻烦

Thread?ThreadLocal?ThreadLocalMap?

在这里插入图片描述
每一个Thread对象都持有一个ThreadLocalMap成员变量。
每一个ThreadLocalMap中存有多个ThreadLocal对象,ThreadLocal做为key,value是泛型。

主要方法

initialValue:

该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get方法的时候,才会触发。

当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种状况下,不会为线程调用initialValue方法。

一般,每一个线程最多调用一次此方法,但若是已经调用了remove()方法,再调用get(),则能够再次调用此方法。

若是不重写本方法,这个方法会返回null,通常使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中能够初始化副本对象。

set:

为这个线程设置新值。

若是须要保存到ThreadLocal里的对象的生成时机不禁咱们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set直接放入咱们的ThreadLocal中去,以便后续使用。

get:

获得这个线程对应的value。若是是首次调用get(),则会调用initialValue来获得这个值。

先取出当前线程的ThreadLocalMap,而后调用map.getEntry()方法,把本ThreadLocal的引用做为参数传入,取出map中属于本ThreadLocal的Value。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

注意:这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中。

remove:

删除对应这个线程的值。

ThreadLocalMap

ThreadLocalMap这个类,也就是Thread.threadLocals

ThreadLocalMap类是每一个线程ThreadL类里面的变量,里面最重要的一个键值对数组Entry[] table,能够认定是一个Map,键值对:
键:这个ThreadLocal
值:实际须要的成员变量。

ThreadLocalMap采用的是线性探测法,也就是若是发生冲突,就继续找下一个空位置,而不是链表拉链。

注意点

内存泄漏

内存泄漏:某个对象再也不有用,可是占用的内存却不能被回收。

经过看源码

//继承WeakReference弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
    	//key使用父类的弱引用
        super(k);
        //value是强引用
        value = v;
    }
}

弱引用的特色是:若是这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就能够被回收。

ThreadLocalMap的每一个Entry都是一个对key的弱引用,同时,每一个Entry都包含了一个对Value的强引用。

正常状况下,当线程终止,保存在ThreadLocal里的Value会被垃圾回收,由于没有任何强引用了。可是,若是线程不终止(好比线程须要保持好久),那么key对应的value就不能被回收,由于有以下调用链:
Thread–>ThreadLocalMap–>Entry(key==null)–>value

由于Value和Thread之间还存在这个强引用链路,因此致使Value没法回收,就可能会出现OOM。

JDK已经考虑到了这个问题,因此在set,remove,rehash方法中会扫描key==null的Entry,并把对应的Value设置为null,这样value对象就能够被回收。

可是若是一个ThreadLocal不被使用,那么实际上set,remove,rehash方法也不会被调用,若是同时线程又不中止,那么调用链就一直存在,那么就致使了Value的内存泄漏。

如何避免?

使用完ThreadLocal以后调用remove()方法进行删除。

文章持续更新,能够微信搜索「 绅堂Style 」第一时间阅读,回复【资料】有我准备的面试题笔记。
GitHub https://github.com/dtt11111/Nodes 有总结面试完整考点、资料以及个人系列文章。欢迎Star。
在这里插入图片描述