并发问题的另一种解决思路-ThreadLocal

436 阅读2分钟

除了控制资源的访问外,我们还可以通过增加资源来保证所有对象的线程安全。比如,让100个人填写个人 信息表,如果只有一只笔,那么大家就得挨个填写,对于管理人员来说,必须保证大家不会去哄抢这仅存的一支笔,否则,谁也填不完。从另外一个角度出发,我们干脆就准备100支笔,人手一支,那么所有人都可以很快完成表格的填写。

如果说锁是使用第一种思路,那么ThreadLocal就是使用第二种思路了。

ThreadLocal的简单使用

public class ThreadLocalDemo {

    private static final ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<>();

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10000; i++) {
            executorService.execute(new ParseDate(i));
        }
        executorService.shutdown();
    }

    static class ParseDate implements Runnable {

        int i = 0;

        public ParseDate(int i) {
            this.i = i;
        }

        @Override
        public void run() {
            try {
                // 1
                if (tl.get() == null) {
                    tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
                }
                Date date = tl.get().parse("2020-06-28 19:25:" + i % 60);
                System.out.println(i+ ":" + date);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    }
}

SimpleDateFormat.parse()并不是线程安全的,我们使用ThreadLocal为每一个线程都产生一个SimpleDateFormat对象实例。

注释1的地方判断如果当前线程不持有SimpleDateFormat对象实例。那么就新建一个并设置到当前线程中,如果已经持有,则直接使用。从这里可以看到,为每一个线程人手分配一个对象的工作并不是由ThreadLocal来完成的,而是需要在应用层面保证的。如果在应用上为每一个线程分配了相同的对象实例,那么ThreadLocal也不能保证线程安全。

为每一个线程分配不同的对象,需要在应用层面保证。ThreadLocal只是起到了简单的容器作用。

ThreadLocal实现原理

关于ThreadLocal我们需要关注的是ThreadLocal的set()和get()两个方法。

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

在set时,首先获得当前线程对象,然后通过getMap()拿到线程的ThreadLocalMap,并将值设入ThreadLocalMap中。

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对象。然后将自己作为key取得内部的实际数据。

当使用ThreadLocal时,维护的变量会维护在Thread类内部的ThreadLocalMap中,这也意味着只要线程不退出,对象的引用就会一直存在。

因此如果我们使用线程池,那就意味着当前线程未必会退出。如果这样,将一些大对象设置到ThreadLocal中,可能会使系统出现内存泄露的可能。

此时,如果你希望及时回收对象,最好使用ThreadLocal.remove()方法将这个变量移除。就像我们习惯性地关闭数据库连接一样。如果你确实不需要这个对象了,那么就应该告诉虚拟机,请把它回收掉,防止内存泄露。

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

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

ThreadLocalMap类似于一个Map,其中的key为当前定义的ThreadLocal变量的this弱引用,value为我们使用set方法设置的值。

回收.png