ThreadLocal【一次解决老大难问题】

一、使用场景

  1. 每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)
  2. 每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。

1.每个线程需要一个独享的对象

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

(1)SimpleDateFormat的进化之路

自己写一个类,实现两个线程分别打印出时间信息

public class ThreadLocalNormalUsage00 {
    public String date(int seconds){
        //Date()参数单位是毫秒,从1970.1.1 00:00:00 计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalNormalUsage00().date(10);
                System.out.println(date);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalNormalUsage00().date(1007);
                System.out.println(date);
            }
        }).start();
    }
}

如果需要写100个呢?那就要在代码中写100个线程和100个SimpleDateFormat,写法不优雅。

    public static void main(String[] args) {
        for (int i=0;i<100;i++) {
            int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage01().date(finalI);
                    System.out.println(date);
                }
            }).start();
        }
    }

用for循环去写100、1000个,那么就需要100次的创建销毁线程,造成很大的开销。所以此时可以引入线程池。

    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 ThreadLocalNormalUsage02().date(finalI);
                    System.out.println(date);
                }
            });
            threadPool.shutdown();
        }
    }

这时候便不会创建1000个线程那么多了,但是还是会创建1000个SimpleDateFormat 对象。如果只创建一个SimpleDateFormat 实例,每次都重复利用就可以节省开销。

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    public String date(int seconds){
        //Date()参数单位是毫秒,从1970.1.1 00:00:00 计时
        Date date = new Date(1000 * seconds);
        return dateFormat.format(date);
    }

就比如这样,将SimpleDateFormat 作为一个静态的属性,但是,这样在运行的时候却发现,存在着相同的时间
在这里插入图片描述
但每次传入的值都是不同的,怎么会有相同的时间?这是因为出现了线程安全问题。如果要解决,是不是可以通过加锁的方式来解决。

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    public String date(int seconds){
        //Date()参数单位是毫秒,从1970.1.1 00:00:00 计时
        Date date = new Date(1000 * seconds);
        String format = null;
        synchronized (ThreadLocalNormalUsage04.class){
            format = dateFormat.format(date);
        }
        return format;
    }

这样一来,最后的结果就是正确的了。不过这样,所有的线程串行执行,效率就很低了。

(2)ThreadLocal粉墨登场

如果在每个线程内部使用一个SimpleDateFormat ,这样既可以保证效率,又不会出现线程安全问题了。

//利用ThreadLocal给每个线程分配自己的SimpleDateFormat对象
public class ThreadLocalNormalUsage05 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    public String date(int seconds){
        //Date()参数单位是毫秒,从1970.1.1 00:00:00 计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }

    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 ThreadLocalNormalUsage05().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }
}
class ThreadSafeFormatter{
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };
}

2.每个线程内需要保存全局变量

ThreadLocal【一次解决老大难问题】_第1张图片
像如图这种需求,从service-1一直到service-4,每次都需要传递user。这样会导致代码冗余且不易维护。

我们可以在每个线程内保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦

可以使用一个静态的ThreadLocal,在线程的生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象作为参数传递的麻烦。

//演示ThreadLocal用法二,避免传参
public class ThreadLocalNormalUsage06 {
    public static void main(String[] args) {
        new Service1().process();
    }
}
class Service1{
    public void process(){
        User user = new User("超哥");
        System.out.println("服务一:"+user.Name);
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}
class Service2{
    public void process(){
        User user = UserContextHolder.holder.get();
        System.out.println("服务二:"+user.Name);
        new Service3().process();
    }
}
class Service3{
    public void process(){
        User user = UserContextHolder.holder.get();
        System.out.println("服务三:"+user.Name);
    }
}
class UserContextHolder{
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

3.ThreadLocal的两个作用(小结)

  1. 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立对象)。
  2. 在任何方法中都轻松获取这个对象。

4.initialValue还是set

(1)initialValue

在ThreadLocal第一次get的时候就把对象初始化出来,对象的初始化时机可以由我们控制。

(2)set

使用set的方式设置对象,可以在对象初始化之后再放入ThreadLocal。

二、ThreadLocal的好处

  1. 线程安全
  2. 不需要加锁,提高执行效率
  3. 更高效利用内存、节省开销
  4. 免去传参的繁琐

三、ThreadLocal源码

1.结构

ThreadLocal【一次解决老大难问题】_第2张图片
每个Thread对象中都持有一个ThreadLocalMap成员变量。

ThreadLocal.ThreadLocalMap threadLocals = null;

一个ThreadLocalMap中会存有多个ThreadLocal,这些ThreadLocal作为Map的key,其中对应的值作为value。

2.主要方法

T initialValue()

  1. 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有调用get()的时候,才会触发。
    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();
    }

观察get()方法的源码可以发现,当这个线程中没有ThreadLocalMap或者其中没有对应此ThreadLocal的Entry,便会返回setInitialValue()。

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

在setInitialValue()中,会调用initialValue(),并且将得到的值放到map中,最后返回value。

  1. 如果已经set()或get()过了,之后调用get()便不会调用initialValue()了。
  2. 但是如果调用remove(),再调用get(),还会调用initialValue()。
  3. 如果不重写此方法,那么该方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。

set(T t)

为这个线程设置一个新的值

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

T get()

功能:得到这个线程对应的value。

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

void remove()

删除当前线程对应的值。

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

this指的是当前的ThreadLocal对象。

四、ThreadLocal注意点

1.内存泄漏

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

(1)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;
            }
        }

(2)value的泄漏

但是在Entry中,value又是强引用。

正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了。但是如果线程不终止(比如线程池里的),那么key对应的value就不能被回收

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

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

(3)如何避免(阿里规约)

调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏。所以使用完ThreadLocal之后,应该调用remove方法。

2.空指针异常

public class ThreadLocalNPE {
    ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
    public void set(){
        longThreadLocal.set(Thread.currentThread().getId());
    }
    public long get(){
        return longThreadLocal.get();
    }

    public static void main(String[] args) {
        ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocalNPE.set();
                System.out.println(threadLocalNPE.get());
            }
        });
        thread1.start();
    }
}

如果我们不set()直接get(),按理来说会返回null。但是这个代码中却会报错,这是因为get()方法的返回类型是long,而不是Long。

所以正确写法是将get()的返回类型设置为Long

3.共享对象

如果set进去的本来就是一个多线程共享的对象(static),那么还是会出现线程安全问题。

五、在Spring中的实例分析

很多地方用到,比如在DateTimeContextHolder类中使用到,还有RequestContextHolder等。

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

你可能感兴趣的:(多线程)