深度剖析ThreadLocal

2019-12-09  本文已影响0人  掩流年

概念

ThreadLocal是并发编程中的一种对象共享方式,从字面意思上看,我们大概能推断出它的用法是作为线程局部变量来使用的。它的好处是,可以存有每个线程独立的数据而互不影响。可以简单的把ThreadLocal理解为一个类似于HashMap的数据结构。更准确的说是WeakHashMap。可以简单来写一个简化版的ThreadLocal

class SimpleThreadLocal {
    static Map<String, String> map = new ConcurrentHashMap<>();
    private static CountDownLatch latch = new CountDownLatch(5);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            int value = i;
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), Integer.toString(value));
                latch.countDown();
            }).start();
        }
        latch.await();
        System.out.println(map);
    }
}

这就可以被作为一个ThreadLocal来使用了,在这个map中,key是它的名称,value可以作为每个线程独有的数据来存储。但实际上ThreadLocal却不是这么的简单,但也复杂不了多少。

深度解析

接下来看一段源码吧

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 getMap(Thread t) {
        return t.threadLocals;
    }

   void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

上述展示了ThreadLocalput方法源码,看起来基本上是一目了然的,基于我们之上手写的ThreadLocal来说,不同的点在于,源码的map是线程独有的,而我们的map是共享的。这时候就要问自己一个问题,为什么要这么设计呢?
答案是,如果采用共享的map,当某个线程被销毁的后,它的数据会占用内存不能被回收。这样导致了很多垃圾数据的积累。所以使用单个线程自带map存储数据是相当方便的。

这时候又出现了新的问题,当我们使用线程池的时候,线程池中的核心线程不会被销毁,有时候线程会把较大的对象存入结构中,长时间的积累将导致内存泄漏。

避免的方式是,ThreadLocal提供了remove()接口,对于不需要的对象,要及时调用remove()方法去清理。

再来看下get()方法的源码

    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();
    }

get()的逻辑从表面看来是个相当容易理解的,重点的深入到ThreadLocalMap中看看getMap的实现。

    static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

上述代码提供了ThreadLocalMap中的一个方法,我们能发现在其中有一个什么精巧的设计。我们可以把k当作线程实例,v当作值。整个Entry继承了WeakReference<ThreadLocal<?>>弱引用,弱引用的方式就是在Java虚拟机GC发现的时候,就可以立即回收。这样当外部的ThreadLocal强引用被回收的时候,这个Entry的key就会被置为null。

应用与注意事项

我们简单举个例子

   private static ThreadLocal<Connection> threadLocal = ThreadLocal.withInitial(() -> {
        try {
            return DriverManager.getConnection("url");
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    });

可以很好的想到,ThreadLocal可以做数据库的连接池。或者web端的session等等。它的作用可以说是十分广泛。
在《Java高并发程序设计》这本书中,有个例子验证了共享一个变量和使用ThreadLocal对性能的影响,有兴趣可以读一读,条件以及得出的结论是:

条件:
4个线程
一千万的随机数循环

结论:
多线程共享一个Random 耗时:`13s`
使用ThreadLocal 耗时:`1.7s`

另一方面,我们需要注意的是,ThreadLocal相当于全局变量,如果滥用它将会导致代码的可重用性降低以及增加耦合度,这是格外需要注意的。

上一篇下一篇

猜你喜欢

热点阅读