(10)ThreadLocal

2020-06-30  本文已影响0人  一个菜鸟JAVA

什么是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方法清除掉其中的数据,从而避免内存泄漏。

上一篇下一篇

猜你喜欢

热点阅读