美文网首页
(10)ThreadLocal

(10)ThreadLocal

作者: 一个菜鸟JAVA | 来源:发表于2020-06-30 20:59 被阅读0次

    什么是ThreadLocal

    ThreadLocal是一个用于创建线程局部变量的类,它有两个特点,其一对于线程A创建的数据只有线程A能获取和修改;其二只要能获取到ThreadLocal的示例,线程就能获取到其中的值。

    使用场景

    网上有介绍ThreadLocal与synchronized对比的文章,但是我觉得它们之间并没有可以性。ThreadLocal注重的点在于通过线程独有空间来存储和获取数据,而synchronized注重的是多线程同时对同一变量的获取与修改。

    简单的比喻是ThreadLocal好比每个人(线程)有自己的口袋存,每个人只通过自己口袋来存放和取东西。而synchronized就好比所有人(多有线程)都在同一个口袋存放和取东西,但是一次只能一个人操作。

    public class App6 {
        public static Map<Thread,String> globalMap = new HashMap<>();
        public static void main(String[] args) throws InterruptedException {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Thread currentThread = Thread.currentThread();
                    //存数据
                    globalMap.put(currentThread,"abc");
                    //取数据
                    System.out.printf("[%s]=[%s]\n",currentThread.getName(),globalMap.get(currentThread));
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Thread currentThread = Thread.currentThread();
                    //存数据
                    globalMap.put(currentThread,"123");
                    //取数据
                    System.out.printf("[%s]=[%s]\n",currentThread.getName(),globalMap.get(currentThread));
                }
            }).start();
            TimeUnit.SECONDS.sleep(1);
            System.out.println(globalMap);
        }
    }
    

    上面的示例中,每个线程将自己的数据放在Map中,然后获取自己数据。但是上面的示例是线程不安全的,千万不要在自己的代码中使用。如果使用上面的方案,我们需要使用线程安全的Map或者加锁来解决。即使解决了线程安全的问题还存在两个问题,其一就是Map中的值并不是当前线程独有的,其他线程也是可以获取和修改它。其二为了保证线程安全Map需要加锁,在性能上时有损失的。
    上面所提到的问题使用ThreadLocal都可以解决。现在修改代码如下:

    public class App7 {
        public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
        public static void main(String[] args) throws InterruptedException {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Thread currentThread = Thread.currentThread();
                    //存数据
                    threadLocal.set("abc");
                    //取数据
                    System.out.printf("[%s]=[%s]\n",currentThread.getName(),threadLocal.get());
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Thread currentThread = Thread.currentThread();
                    //存数据
                    threadLocal.set("123");
                    //取数据
                    System.out.printf("[%s]=[%s]\n",currentThread.getName(),threadLocal.get());
                }
            }).start();
            TimeUnit.SECONDS.sleep(1);
            System.out.println(threadLocal.get());
        }
    }
    

    通过使用ThreadLocal,它既能保证多线程访问的安全性,同时也能实现无锁。

    实现原理

    要想实现ThreadLocal这样的效果我们基本山能想到的就两种方式方式来实现。

    ThreadLocal维护Thread与数据的映射关系

    该实现方式是在ThreadLocal内部维护一个线程安全的Map,然后以当前线程作为Map的Key,而线程的数据作为Value。

    ThreadLocal维护Thread与数据的映射关系.jpg

    上面的这种方式能实现ThreadLocal的功能,但是问题在于通过这种方式实现就必须要保证ThreadLocal中负责维护线程和数据的Map线程安全,这或多或少都需要增加锁的引入,并不能实现无锁。

    Thread维护ThreadLocal与数据的映射关系

    通过上面的分析我们知道如果在ThreadLocal中维护Thread与数据的映射我们需要必须要保证内部映射关系的线程安全,如果我们在Thread内存维护一个ThreadLocal与数据之前的映射关系,这种映射关系并没有涉及到线程安全问题,这样也就省去了线程同步的操作,相比上面的实现方式,该方式性能上更好。而JDK内部就是使用该方式来实现的。

    Thread维护ThreadLocal与数据的映射关系.jpg

    基本使用

    实例化

    示例化方式一般就两种,第一种是直接使用无参构造函数创建,第二种则是在1.8的版本提供的静态方法创建。

    public class App8 {
        /**
         * 实例化方式1 构造函数
         */
        public static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
        /**
         * 实例化方式2 静态方法,
         */
        public static ThreadLocal<String> threadLocal2 = ThreadLocal.withInitial(() -> {
            System.out.println("如果值为空时则会调用该方法获取初始值");
            return "abc";
        });
        public static void main(String[] args) {
            System.out.println(threadLocal1.get());
            System.out.println(threadLocal2.get());
            threadLocal2.remove();
            System.out.println(threadLocal2.get());
        }
    }
    

    常用方法

    常用的方法就三个,set()用来往ThreadLocal中设置值,get()用来往ThreadLocal中获取值,而remove()用来删除ThreadLocal中的值。

    public class App9 {
        public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
        public static void main(String[] args) {
            //设置值
            threadLocal.set("abc");
            //获取值
            String value = threadLocal.get();
            System.out.println(value);
            //删除值
            threadLocal.remove();
            System.out.println(threadLocal.get());
        }
    }
    

    ThreadLocal中提供的方法比较简单,但是在使用时需要特别注意remove方法。当我们使用完ThreadLocal后我们应该调用remove方法将ThreadLocal中的数据清除,如果不这么做容易产生业务数据异常和内存泄漏(后面将说明为什么会导致内存泄漏)。

    public class App10 {
        public static ThreadLocal<List<Integer>> threadLocal = ThreadLocal.withInitial(() -> new ArrayList<>());
        public static void main(String[] args) {
            ExecutorService executor = Executors.newFixedThreadPool(2);
            for (int i = 0; i < 10; i++) {
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        int value = new Random().nextInt(1000);
                        threadLocal.get().add(value);
                        System.out.println(threadLocal.get());
                    }
                });
            }
            executor.shutdown();
        }
    }
    

    最后的打印结果如下:

    [562]
    [725]
    [562, 434]
    [725, 590]
    [562, 434, 448]
    [725, 590, 377]
    [562, 434, 448, 712]
    [725, 590, 377, 57]
    [562, 434, 448, 712, 715]
    [725, 590, 377, 57, 580]

    因为我们是在线程池中使用ThreadLocal,而线程池中的线程并不是执行完之后就销毁了。而代码中并没有调用remove方法清除ThreadLocal中的值,这就导致了List中保留了上一次任务的执行结果。

    源码分析

    我们在实现原理中大致讲过ThreadLocal是如何实现的,我们先看如何往ThreadLocal中存储值的。

    public void set(T value) {
        //获取当前的线程
        Thread t = Thread.currentThread();
        //获取Map
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //往Map中放值,Map的key就是当前的ThreadLocal实例
            map.set(this, value);
        else
            //如果Map为空则创建Map,并将值放入Map中
            createMap(t, value);
    }
    

    上面代码中的getMap(t)方法的实现如下:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    

    代码很简单,返回的就是ThreadLocal中的字段threadLocals,它的类型就是ThreadLocalMap。我们调用set方法就是往Map中存放存放值,而这个Map的key就是ThreadLocal,它的值就是我们要存的值。

    public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取Map
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //获取Entry中的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果Map为空则获取withInitial中supplier返回的值
        return setInitialValue();
    }
    

    为何会内存泄漏

    通过上面我们知道了ThreadLocal的实现原理,但是为何说会内存泄漏呢?我们先看ThreadLocalMap的结构。

    ThreadLocalMap内部结构.png

    上图就是ThreadLocal的结构了,它内部提供了增删改等主要方法,而ThreadLocal与值被封装成Entry对象存放在table数组中。

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

    从Entry中的结构可以知道,每一个Entry对象都是一个对Key弱的引用(关于什么是弱引用可以参考该文),当没有强引用指向ThreadLocal变量时,ThreadLocal可以被回收。真是通过这种方式保证了ThreadLocal在没有引用时而Thread还没有被销毁时可以被回收。但是上面的问题带来了另一个问题,当Key被回收之后Entry对象并没有被回收而导致内存泄漏。

    如何解决

    对于ThreadLocal中存在的内存泄漏问题,最简单的解决方案就是我们在每次在使用完ThreadLocal后手动的调用remove清除数据。但是如果你没有这么做,ThreadLocal对于存在的内存泄漏问题也做了部分优化。在set和get方法中,都会间接或直接的调用cleanSomeSlots、expungeStaleEntry、replaceStaleEntry等方法将key为空的Entry清除掉。虽然对于内存泄漏ThreadLocal内部已经做了优化,但是我们在使用最好还是在ThreadLocal不再使用时手动调用remove方法清除掉其中的数据,从而避免内存泄漏。

    相关文章

      网友评论

          本文标题:(10)ThreadLocal

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