深入理解ThreadLocal

一、 对ThreadLocal的理解

ThreadLocal,很多叫做线程本地变量,也有叫线程本地存储,可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量。

public class ConnectionManager {
    private static Connection connect = null;
    
    public static Connection openConnection() {
        if(connect == null){
            connect = DriverManager.getConnection();
        }
        return connect;
    }
     
    public static void closeConnection() {
        if(connect!=null)
            connect.close();
    }
}

这样一个数据库连接管理类,多线程会存在线程安全问题:第一,这里面的2个方法没有进行同步,很可能再openConnection方法中会多次创建Connect,第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭连接。

所以出于线程安全的考虑,必须将这段代码的两个方法进行同步处理,并且在调用connect的地方需要进行同步处理。

这样将会大大影响程序执行效率,因为一个线程在使用connect进行数据库操作的时候,其他线程只能等待。

那么,这地方到底需不需要将connect变量进行共享呢?事实上,是不需要的。假如每个线程都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关系其他线程是否对这个connect进行了修改的。

到这里,大家可能会想到,既然不需要在线程之间共享这个变量,可以直接这样处理,在每个需要使用数据库连接的方法中使用时才创建数据库连接人,然后再方法调用完毕再释放这个连接。

public class Dao {
    public void insert() {
        ConnectionManager connectionManager = new ConnectionManager();
        Connection connection = connectionManager.openConnection();
         
        //使用connection进行操作
         
        connectionManager.closeConnection();
    }
}

这样处理没有任何问题,由于每次都是在方法内部创建的连接,那么线程之间自然不存在线程安全问题,但这样有个致命的影响:导致服务器压力过大,并且严重影响程序执行想能,由于在方法中需要频繁的开启和关闭数据库连接,这样严重影响程序执行效率,还可能导致服务器压力巨大。

这种情况下,使用ThreadLocal是再适合不过了,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能,

但也要注意,虽然ThreadLocal能够解决上边的问题,但由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比较大等。

二、深入解析ThreadLocal类源码

ThreadLocal类的几个方法:

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }

get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,

set()用来设置当前线程中变量的副本,

remove()用来移除当前线程中变量的副本

initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载的方法。

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    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();
    }

第一句获取当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap,然后接着下边取到<key,value>键值对,这里获取键值对传进去的是this,而不是当前线程t

如果获取成功,则返回value值

如果map为空,则调用setInitialValue方法返回value

getMap方法做了什么

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

getMap中调用了当前线程t,返回当前线程t中的一个成员变量threadlocals,那么继续看Thread类中取看一个成员变量threadlocals是什么:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

实际上就是一个ThreadLocalMap,这个类型是ThreadLocal类的一个内部类,我们继续看ThreadLocalMap的实现

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

可以看到ThreadLocalMap的entry继承了WeakReference,并且使用了ThreadLocal作为键值。

ThreadLocalMap和HashMap不同的是:ThreadLocalMap的每个Entry都是一个对key的弱引用,这个一点从super(key)可以看到,另外,每个entry都包含了一个对value的强引用

使用弱引用的原因:当没有强引用指向ThreadLocal变量时,它可被回收,从而避免上文所述的ThreadLocal不能被回收造成内存泄漏问题。

但是,这里还可能有另一种内存泄漏问题,ThreadLocalMap变量维护ThreadLocal变量和具体实例的映射,当ThreadLocal变量被回收时,该映射的键为null,该Entry无法被移除,从而使得实际被Entry引用而无法被回收造成的内存泄漏。

需注意:Entry虽然是弱引用,但它是对ThreadLocal类型的弱引用(也即是对键的弱引用),而非具体实例的弱引用,所以无法避免具体实例相关的内存泄漏问题。

然后继续setInitialValue方法的具体实现:

/**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

如果map不为空,就设置键值对,为空,再创建Map,看一下createMap的实现:

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

至此,可能大部分朋友已经明白了ThreadLocal是如何为每个线程创建变量的副本的:

首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadlocals,这个threadlocals就是用来存储实际的变量副本的,键值为当前Threadlocal变量,value为变量副本(即T类型的变量)

初始时,在Thread里面,threadlocals为空,当通过ThreadLocal变量调用get方法或者set方法,就会对Thread类中的threadlocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadlocals.

总结下:

1、实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadlocals中的

2、为何threadlocals的类型是ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadlocal变量

3、在get之前,必须先set,否则会报空指针异常,如果在get之前不需要调用set的话,必须重写initialValue方法。

4、ThreadLocal 并不解决线程间共享数据的问题

5、ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题

6、每个线程持有一个 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题

7、ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题

8、ThreadLocalMap 的 set 方法通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏

9、ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景

三、ThreadLocal的应用场景

最常见的ThreadLocal使用场景为用来解决:数据库连接,Session管理等。

 

 

posted @ 2018-05-14 16:19  提拉没有米苏  阅读(177)  评论(0编辑  收藏  举报