JUC-ThreadLocal

ThreadLocal 的使用

ThreadLocal的用途—两大使用场景

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

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

比喻:教材只有一本,一起做笔记有线程安全问题。复印后没问题。

  1. 2个线程分别用自 己的SimpleDateFormat ,这没问题

  2. 后来延伸出10个 ,那就有10个线程和10个 SimpleDateFormat ,这虽然写法不优雅(应该复用对 象) ,但勉强可以接受

  3. 但是当需求变成了 1000个,那么必然要用线程池(否则 消耗内存太多)

  4. 所有的线程都共用同一个simpleDateFormat对象,有线程安全问题

    用synchronized 加锁解决的话,效果正常,但是效率低

    用ThreadLocal解决问题,可以让每个线程独享simpleDateFormat对象

    当多个不同线程用同一个SimpleDateFormat会引发线程安全问题

    利用ThreadLocal解决线程安全问题,并且不怎么影响性能。

    代码实现:通过在类中实现ThreadLocal,重写initialValue,用的时候用ThreadLocal.get()

    package threadlocal;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * 描述:     利用ThreadLocal,给每个线程分配自己的SimpleDateFormat对象,保证了线程安全,高效利用内存
     */
    public class ThreadLocalNormalUsage05 {
    
        public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    
        public static void main(String[] args) throws InterruptedException {
            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();
        }
    
        public String date(int seconds) {
            //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
            Date date = new Date(1000 * seconds);
    //        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
            return dateFormat.format(date);
        }
    }
    
    class ThreadSafeFormatter {
    	//实现ThreadLocal,泛型用我们需要的对象,实现initialValue方法
        public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            }
        };
    	//Java8
        public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
                .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    }
    

场景2: 每个线程内需要保存全局变量

实例:当前用户信息需要被线程内所有方法共享

  • 个比较繁琐的解决方案是把user作为参数层层传递,从service-1()传 到service- 2(),再从service- 2()传到service-3() ,以此类推,但是这样 做会导致代码冗余且不易维护

方法:用ThreadLocal保存一些业务内容(用户权限信息,从用户系统获取用户名,userId等)

这些信息在同一个线程中相同,但是在不同线程中使用业务内容时不相同的。但是我们需要他们相同。

当多线程同时工作时,我们需要保证线程安全,可以用synchronized,也可以使用ConcurrentHashMap,但无论用什么对性能都有一定的影响。

更好的办法是使用ThreadLocal,这样无需synchronized,可以在不影响性能的情况下,无序层层传递参数,就可达到保存当前线程对应用户信息的目的。

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

和场景1的代码实现方式不同,我们不需要实现**initialValue()方法,但是必须手动调用Set()**方法。

package threadlocal;

/**
 * 描述:     演示ThreadLocal用法2:避免传递参数的麻烦
 */
public class ThreadLocalNormalUsage06 {
    public static void main(String[] args) {
        new Service1().process("牛叠为");
    }
}

class Service1 {
    public void process(String name) {
        User user = new User(name);
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}
class Service2 {
    public void process() {
        User user = UserContextHolder.holder.get();
        ThreadSafeFormatter.dateFormatThreadLocal.get();
        System.out.println("Service2拿到用户名:" + user.name);
        new Service3().process();
    }
}
class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用户名:" + user.name);
        UserContextHolder.holder.remove();
    }
}

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

class User {

    String name;

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

使用总结

  1. 让某个需要用到的对象在线程间隔离
  2. 在任何方法中都可以轻松获取到该对象
  3. 达到线程安全,无需加锁,提高执行效率,更高效的利用内存,节省开销。 如: 不用频繁创建SimpleDateFormat
  4. 免去传参的繁琐:无论是场景一的工具类,还是场景二的用户名,都可以在任何地方直接通过ThreadLocal拿到,再也不需要每次都传同样的参数。ThreadLocal使得代码耦合度更低,更优雅。

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

场景2:set 需要保存到ThreadLocal里的对象生成时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set直接放到我们的ThreadLocal中,以便后续使用。

ThreadLocal原理源码分析

Thread,ThreadLocal,ThreadLocalMap之间的关系

Thread类中都持有一个ThreadLocalMap成员变量

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
 ThreadLocal.ThreadLocalMap threadLocals = null;

即ThreadLocalMap可以保存多个ThreadLocal

主要方法

  • T initialValue() 初始化 需要重写
  1. 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发

    protected T initialValue() {
            return null;
    }
    
  2. 当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法

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

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

  • void set(T t): 为这个线程设置一个新值
//拿到当前线程的ThreadLocalMap 第一次设置就创建Map,把本ThreadLocal的引用作为参数传入,并把value设置进去
//最后将ThreadLocal存进ThreadLocalMap中
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(),则会调用initialize来得到这个值

    /*
    get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value
    注意,这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中
    */
    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();
    }
    
    /**
         * Variant of set() to establish initialValue. Used instead
         * of set() in case user has overridden the set() method.
         *
         * @return the initial value
         */
    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);
            }
            if (this instanceof TerminatingThreadLocal) {
                TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
            }
            return value;
        }
    
  • void remove(): 删除对应这个线程的值

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

ThreadLocalMap 类分析

也就是Thread.threadLocals

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

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

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

通过源码分析可以看出,setInitialValue和直接set最后都是利用map.set()方法来设置值 也就是说,最后都会对应到ThreadLocalMap的一个Entry,只不过是起点和入口不一样

ThreadLocal注意点

  • 内存泄露

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

    • ThreadLocalMap的Key的泄漏: ThreadLocalMap中的Entry继承自WeakReference,是弱引用

    • 弱引用的特点是,如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收。

      所以弱引用不会阻止GC,因此这个弱引用的机制

    • 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方法

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

  • 空指针异常

    • 在进行get之前,必须先set,否则可能会报空指针异常。
  • 共享对象

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

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

  • 优先使用框架的支持,而不是自己创造 例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄漏

    • DateTimeContextHolder类,看到里面用了ThreadLocal
    • 每次HTTP请求都对应一个线程,线程之间相互隔离,这就是ThreadLocal的典型应用场景
全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务