美文网首页
Threadlocal 研究1

Threadlocal 研究1

作者: 巴巴11 | 来源:发表于2020-04-16 23:44 被阅读0次

    1 使用场景

    每个线程都需要独享一个对象,来达到线程安全的目的。
    每个线程都有对象的副本。
    比喻:教材只有一本,一起做笔记有线程安全问题。复印后没有问题,使用ThradLocal相当于复印了教材。

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

    java.lang.ThreadLocal<T>

    场景1代码:

    线程不安全的代码

    package com.mxixm.spring.mvc;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class Main2 {
        static ExecutorService pool = Executors.newFixedThreadPool(10);
        static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    
        public static void main(String[] args) {
            for (int i =0;i < 1000; i++) {
                int d = i;
                pool.submit(new Runnable() {
                    @Override
                    public void run() {
                        String date = new Main2().convert(d);
                        System.out.println(date);
                    }
                });
            }
            pool.shutdown();
        }
    
        public String convert(int seconds) {
            Date date = new Date(1000 * seconds);
            return format.format(date);
        }
    }
    
    

    改进1

    // 改进1,加锁
        public String convert1(int seconds) {
            Date date = new Date(1000 * seconds);
            String s = "";
            synchronized (Main2.class) {
                s = format.format(date);
            }
            return s;
        }
    

    改进2:使用Threadlocal

    package com.mxixm.spring.mvc;
    
            import java.text.SimpleDateFormat;
            import java.util.Date;
            import java.util.concurrent.ExecutorService;
            import java.util.concurrent.Executors;
    
    public class Main2 {
        static ExecutorService pool = Executors.newFixedThreadPool(10);
        static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    
        public static void main(String[] args) {
            for (int i =0;i < 1000; i++) {
                int d = i;
                pool.submit(new Runnable() {
                    @Override
                    public void run() {
                        String date = new Main2().convert(d);
                        System.out.println(date);
                    }
                });
            }
            pool.shutdown();
        }
    
        public String convert(int seconds) {
            Date date = new Date(1000 * seconds);
            return format.format(date);
        }
    
        // 改进1,加锁
        public String convert1(int seconds) {
            Date date = new Date(1000 * seconds);
            String s = "";
            synchronized (Main2.class) {
                s = format.format(date);
            }
            return s;
        }
    
        /**
         * 利用 ThreadLocal 给每个线程分配自己的 dateFormat 对象
         * 不但保证了线程安全,还高效的利用了内存
         */
        public String convert2(int seconds) {
            Date date = new Date(1000 * seconds);
            SimpleDateFormat format = ThreadLocalFormatter.formatThreadLocal.get();
            return format.format(date);
        }
    
    }
    
    class ThreadLocalFormatter {
        public static ThreadLocal<SimpleDateFormat> formatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
            }
        };
    }
    

    场景2代码:
    当前用户信息需要被线程内的所有方法共享

    如果考虑用一个map来存,在多线环境下有问题,如果用加锁和ConcurrentHashMap会带来性能问题。

    实例:

    package com.mxixm.spring.mvc;
    
    public class Main3 {
        public static void main(String[] args) {
            new Service1().proc1();
        }
    }
    
    class Service1 {
        public void proc1() {
            User user = new User("test");
            System.out.println("proc 1 ...." + user.name);
            ThreadLocalContext.threadLocal.set(user);
            new Service2().proc2();
        }
    }
    
    class Service2 {
        public void proc2() {
            User user = ThreadLocalContext.threadLocal.get();
            System.out.println("proc 2 ...." + user.name);
            new Service3().proc3();
        }
    }
    
    class Service3 {
        public void proc3() {
            User user = ThreadLocalContext.threadLocal.get();
            System.out.println("proc 3 ...." + user.name);
        }
    }
    
    class User {
        String name;
        public User(String name) {
            this.name = name;
        }
    }
    
    class ThreadLocalContext {
        public static ThreadLocal<User> threadLocal = new ThreadLocal<>();
    }
    

    3 总结

    • 可以做到线程安全,每个线程都有一个对象的副本
    • 可以在任何方法中轻松获取到对象(get方法)
    • 可以根据对象生成的时机选择使用initalValue方法还是set方法
    • 对象初始化的时机由我们控制的时候使用initialValue 方式
    • 如果对象生成的时机不由我们控制的时候使用 set 方式

    4 Threadlocal好处

    • 线程安全
    • 不需要加锁,性能搞
    • 节省内存
    • 免去传参的繁琐,降低耦合

    5 原理

    image.png

    每个Thread类内部都有一个ThreadLocalMap对象。
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocalMap用于存储ThreadLocal。
    因为在同一个线程当中可以有多个ThreadLocal,并且多次调用get()所以需要在内部维护一个ThreadLocalMap用来存储多个ThreadLocal。

    6 相关方法

    T initialValue()

    该方法用于设置初始值,并且在调用get()方法时才会被触发,所以是懒加载。
    但是如果在get()之前进行了set()操作,这样就不会调用initialValue()。
    通常每个线程只能调用一次本方法,但是调用了remove()后就能再次调用

    void set(T t)
    为这个线程设置一个新值

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

    T get()
    获取线程对应的value

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

    void remove()
    删除对应这个线程的值

    public void remove() {
            ThreadLocal.ThreadLocalMap m = this.getMap(Thread.currentThread());
            if (m != null) {
                m.remove(this);
            }
    
        }
    
    private void remove(ThreadLocal<?> key) {
                ThreadLocal.ThreadLocalMap.Entry[] tab = this.table;
                int len = tab.length;
                int i = key.threadLocalHashCode & len - 1;
    
                for(ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
                    if (e.get() == key) {
                        e.clear();
                        this.expungeStaleEntry(i);
                        return;
                    }
                }
    
            }
    
    static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
                //调用父类,父类是一个弱引用
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    this.value = v;
                }
            }
    

    6 注意事项

    内存泄漏问题
    内存泄露;某个对象不会再被使用,但是该对象的内存却无法被收回

    强引用:当内存不足时触发GC,宁愿抛出OOM也不会回收强引用的内存
    弱引用:触发GC后便会回收弱引用的内存
    正常情况
    当Thread运行结束后,ThreadLocal中的value会被回收,因为没有任何强引用了
    非正常情况
    当Thread一直在运行始终不结束,强引用就不会被回收,存在以下调用链 Thread-->ThreadLocalMap-->Entry(key为null)-->value因为调用链中的 value 和 Thread 存在强引用,所以value无法被回收,就有可能出现OOM。
    JDK的设计已经考虑到了这个问题,所以在set()、remove()、resize()方法中会扫描到key为null的Entry,并且把对应的value设置为null,这样value对象就可以被回收。
    但是只有在调用set()、remove()、resize()这些方法时才会进行这些操作,如果没有调用这些方法并且线程不停止,那么调用链就会一直存在,所以可能会发生内存泄漏。

    private void resize() {
                ThreadLocal.ThreadLocalMap.Entry[] oldTab = this.table;
                int oldLen = oldTab.length;
                int newLen = oldLen * 2;
                ThreadLocal.ThreadLocalMap.Entry[] newTab = new ThreadLocal.ThreadLocalMap.Entry[newLen];
                int count = 0;
                ThreadLocal.ThreadLocalMap.Entry[] var6 = oldTab;
                int var7 = oldTab.length;
    
                for(int var8 = 0; var8 < var7; ++var8) {
                    ThreadLocal.ThreadLocalMap.Entry e = var6[var8];
                    if (e != null) {
                        ThreadLocal<?> k = (ThreadLocal)e.get();
                        //当ThreadLocal为空时,将ThreadLocal对应的value也设置为null
                        if (k == null) {
                            e.value = null; // Help the GC
                        } else {
                            int h;
                            for(h = k.threadLocalHashCode & newLen - 1; newTab[h] != null; h = nextIndex(h, newLen)) {
                            }
    
                            newTab[h] = e;
                            ++count;
                        }
                    }
                }
    
                this.setThreshold(newLen);
                this.size = count;
                this.table = newTab;
            }
    

    使用ThreadLocal之后,都应该调用remove方法(阿里规范)

    class Service3 {
        public void proc3() {
            User user = ThreadLocalContext.threadLocal.get();
            System.out.println("proc 3 ...." + user.name);
            // 调用完ThreadLocal后,都应该调用remove方法
            // 手动释放内存,从而避免内存泄漏
            ThreadLocalContext.threadLocal.remove();
        }
    }
    

    ThreadLocal的空指针异常问题

    如果get方法返回值为基本类型,则会报空指针异常,如果是包装类型就不会出错。这是因为基本类型和包装类型存在装箱和拆箱的关系,造成空指针问题的原因在于使用者。

    package com.mxixm.spring.mvc;
    
    import java.net.ServerSocket;
    
    /**
     * ThreadLocal的空指针异常问题
     */
    public class Main4 {
        ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
    
        void set() {
            longThreadLocal.set(Thread.currentThread().getId());
        }
    
        Long get() {
            return longThreadLocal.get();
        }
    
        public static void main(String[] args) {
            Main4 main4 = new Main4();
            //如果get方法返回值为基本类型,则会报空指针异常,如果是包装类型就不会出错
            System.out.println(main4.get());
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    main4.set();
                    System.out.println(main4.get());
                }
            }).start();
        }
    }
    
    

    共享对象问题

    如果在每个线程中ThreadLocal.set()进去的东西本来就是多个线程共享的同一对象,比如static对象,那么多个线程调用ThreadLocal.get()获取的内容还是同一个对象,还是会发生线程安全问题。

    可以不使用ThreadLocal就不要强行使用

    如果在任务数很少的时候,在局部方法中创建对象就可以解决问题,这样就不需要使用ThreadLocal。

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

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

    相关文章

      网友评论

          本文标题:Threadlocal 研究1

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