美文网首页Java-多线程
Java并发编程 ThreadLocal

Java并发编程 ThreadLocal

作者: 香沙小熊 | 来源:发表于2020-03-05 11:58 被阅读0次

    1.ThreadLocal的用途

    场景1:
    每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)

    看问题代码

    打印1000个不同线程

    public class ThreadLocalNormalUsage03 {
    
        public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
        static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
        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 ThreadLocalNormalUsage03().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);
            return dateFormat.format(date);
        }
    }
    
    image.png
    所用的线程都共用同一个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 {
        /**
         * 写法一
         */
        public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            }
        };
        /**
         * Lambda表达式
         */
        public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
                .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    }
    

    场景2:
    每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦
    实例:当前用户信息需要被线程内所有方法共享

    public class ThreadLocalNormalUsage06 {
    
        public static void main(String[] args) {
            new Service1().process("kpioneer");
            new Service2().process();
            new Service3().process();
        }
    }
    
    class Service1 {
        public void process(String name) {
            User user = new User(name);
            UserContextHolder.holder.set(user);
            System.out.println("Service1设置用户名:" + user.name);
        }
    }
    
    class Service2 {
    
        public void process() {
            User user = UserContextHolder.holder.get();
            ThreadSafeFormatter.dateFormatThreadLocal.get();
            System.out.println("Service2拿到用户名:" + user.name);
        }
    }
    
    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;
        }
    }
    
    Service1设置用户名:kpioneer
    Service2拿到用户名:kpioneer
    Service3拿到用户名:kpioneer
    

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

    2. ThreadLocal的两个作用

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

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

    场景二:set

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

    3.主要方法介绍

    initialValue()
    • 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发。
    • 当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这** 种情况下,不会为线程调用本initialValue方法
    • 通常,每个线程最多调用一次此方法,但如果调用了remove()后,再调用get(),则可以再次调用此方法
    • 如果不重写本方法,这个方法会返回null。
    void set(T t)

    设置当前线程的threadLocal变量为指定值.

    get

    返回当前线程的这个threadLocal变量的映射值. 如果变量没有当前线程的值,它第一次初始化的值是由initialValue方法的调用返回

        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();
        }
    
    remove 移除当前线程的threadLocal值
         public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 m.remove(this);
         }
    

    4. 原理、源码分析

    • 每个Thread对象都持有一个ThreadLocalMap成员变量
    • ThreadLocalMap存放ThreadLocal对象为key,initialValue对象或者set对象为value
    • ThreadLocalMap存多个ThreadLocal
    public class Thread implements Runnable {
         ...
        ThreadLocal.ThreadLocalMap threadLocals = null;
         ...
    }
    
        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;
        }
    
        /**
         * Sets the current thread's copy of this thread-local variable
         * to the specified value.  Most subclasses will have no need to
         * override this method, relying solely on the {@link #initialValue}
         * method to set the values of thread-locals.
         *
         * @param value the value to be stored in the current thread's copy of
         *        this thread-local.
         */
        public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
    
    ThreadLocalMap的底层源码分析

    ThreadLocalMap是ThreadLocal内部的一个Map实现,然而它没有实现任何集合的接口规范,因为它仅供ThreadLocal内部使用,数据结构采用数组+开方地址法(线性探测法),Entry继承WeakRefrence,是基于ThreadLocal这种特殊场景实现的Map,它的实现方式很值得我们取研究!

    ThreadLocalMap中Entry的源码
            static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    

    源码分析:

    1.Entry中key只能是ThreadLocal对象,被规定死了的

    2.Entry继承了WeakRefrence(弱引用,生存周期只能活到下次GC前),但是只有Key是弱引用,Value并不是弱引用

    ps:value既然不是弱引用,那么key在被回收之后(key=null)Value并没有被回收,如果当前线程被回收了那还好,这样value也和线程一起被回收了,要是当前线程是线程池这样的环境,线程结束没有销毁回收,那么Value永远不会被回收,当存在大量这样的value的时候,就会产生内存泄漏,那么Java 8中如何解决这个问题的呢?

            private void set(ThreadLocal<?> key, Object value) {
    
    
                Entry[] tab = table;
                int len = tab.length;
                int i = key.threadLocalHashCode & (len-1);
    
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {
                    ThreadLocal<?> k = e.get();
    
                    if (k == key) {
                        e.value = value;
                        return;
                    }
    
                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
    
                tab[i] = new Entry(key, value);
                int sz = ++size;
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    

    以上是ThreadLocalMap的set方法,for循环遍历整个Entry数组,遇到key=null的就会替换,这样就不存在value内存泄漏的问题了!

    ThreaLocalMap中key的HashCode计算

    ThreadLocalMap这里采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链法。
    ThreaLocalMap的key是ThreaLocal,它不会传统的调用ThreadLocal的hashcode方法(继承自object的hashcode),而是调用nexthashcode

    private final int threadLocalHashCode = nextHashCode();
    
     private static AtomicInteger nextHashCode = new AtomicInteger();
    
     //1640531527 这是一个神奇的数字,能够让hash槽位分布相当均匀
     private static final int HASH_INCREMENT = 0x61c88647; 
    
     private static int nextHashCode() {
            return nextHashCode.getAndAdd(HASH_INCREMENT);
     }
    
    private void set(ThreadLocal<?> key, Object value) {
    
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1); // 用key的hashCode计算槽位
        // hash冲突时,使用开放地址法
        // 因为独特和hash算法,导致hash冲突很少,一般不会走进这个for循环
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
    
            if (k == key) { // key 相同,则覆盖value
                e.value = value; 
                return;
            }
    
            if (k == null) { // key = null,说明 key 已经被回收了,进入替换方法
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        // 新增 Entry
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些过期的值,并判断是否需要扩容
            rehash(); // 扩容
    }
    

    源码分析:

    1.先是计算槽位

    2.Entry数组中存在需要插入的key,直接替换即可,存在key=null,也是替换(可以避免value内存泄漏)

    3.Entry数组中不存在需要插入的key,也没有key=null,新增一个Entry,然后判断一下需不需要扩容和清除过期的值

    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key) // 无hash冲突情况
            return e;
        else
            return getEntryAfterMiss(key, i, e); // 有hash冲突情况
    }
    

    1.计算槽位i,判断table[i]是否有目标key,没有(hahs冲突了)则进入getEntryAfterMiss方法

    5. 注意事项

    如何避免内存泄露

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

    空指针异常
    public class ThreadLocalNPE {
    
        ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>();
    
        public void set() {
            longThreadLocal.set(Thread.currentThread().getId());
        }
    
        public long get() {
            return longThreadLocal.get();
        }
    
        public static void main(String[] args) {
            ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
            System.out.println(threadLocalNPE.get());
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    threadLocalNPE.set();
                    System.out.println(threadLocalNPE.get());
                }
            });
            thread1.start();
        }
    }
    
    
    Exception in thread "main" java.lang.NullPointerException
        at com.kpioneer.thread.threadlocal.ThreadLocalNPE.get(ThreadLocalNPE.java:15)
        at com.kpioneer.thread.threadlocal.ThreadLocalNPE.main(ThreadLocalNPE.java:20)
    

    因为long是基本类型,这里threadLocalNPE.get()返回值null,且类型是Long为包装类型,在拆箱过程中转化为long类型,报空指针错误。

    共享对象

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

    6. 其它

    • 如果可以不使用ThreadLocal就解决问题,那么不要强行使用
      例如在任务数很少时,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到ThreadLocal
    • 优先使用框架的支持,而不是自己创造
      例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄露。

    相关文章

      网友评论

        本文标题:Java并发编程 ThreadLocal

        本文链接:https://www.haomeiwen.com/subject/qskilhtx.html