Java中的ThreadLocal和InheritableThreadLocal

Threadlocal是什么

java中处理多线程资源共享主要有两种方法,一种是同步,另一种就是使用threadlocal,锁机制采用了“以时间换空间”的方式,仅提供一份变量,让不同的线程排队访问。而Threadlocal采用了“以空间换时间”的方式,为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map(ThreadLocalMap),用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。

Threadlocal简单实现

按照上面描述的原理,我们可以实现一个简单的threadlocal如下:

public class SimpleThreadLocal {

    private Map<Thread, Object> map = new ConcurrentHashMap();


    public void set(Object o) {
        //键为线程对象,值为本线程的变量副本
        map.put(Thread.currentThread(), o);
    }

    public Object get() {
        Thread currentThread = Thread.currentThread();
        //返回本线程对应的变量
        Object value = map.get(currentThread);
        if (value == null && !map.containsKey(currentThread)) {
            //如果在Map中不存在,放到Map中保存起来。
            value = initialValue();
            map.put(currentThread, value);
        }
        return value;
    }

    public void remove() {
        map.remove(Thread.currentThread());
    }

    private Object initialValue() {
        return null;
    }
}

这个Threadlocal虽然有点幼稚,但是已经和JDK的Threadlocal很接近了。

ThreadLocal底层实现

那么到底ThreadLocal类是如何实现这种“为每个线程提供不同的变量拷贝”的呢?下面我们分析一下ThreadLocal的具体实现细节,首先展示了ThreadLocal提供的一些方法,我们重点关注的是get、set、remove方法。

构造函数

    public ThreadLocal() {
    }

很遗憾构造函数什么都没做,那么初始化阶段势必在set操作时完成。

set

    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) {
//获取和设置Thread内的一个叫threadLocals的变量,而这个变量的类型就是ThreadLocalMap,也就是说每个线程都有自己独立的ThreadLocalMap对象
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。

上面代码出现的ThreadLocalMap是什么,?

ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。

ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。

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


    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;
    }
    
    protected T initialValue() {
        return null;
    }

在获取和当前线程绑定的值时,ThreadLocalMap对象以this指向的ThreadLocal对象为键进行查找,这和前面set()方法的代码相互呼应。如果不存在就返回默认的null值,initialValue方法可以在初始化时被重写,从而实现自定义的默认值。

进一步地,我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离,为什么可以这么做?

因为不同的ThreadLocal对象作为不同键,当然也可以在线程的ThreadLocalMap对象中设置不同的值了。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个HashMap对象中存储一个键值对和多个键值对一样,仅此而已。

remove

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

清除当前线程的ThreadLocal

InheritableThreadLocal

ThreadLocal固然很好,但是子线程并不能取到父线程的ThreadLocal的变量。使用InheritableThreadLocal时可以做到的在父子线程之间传递数据。
如下demo:


public class InheritableThreadLocalDemo {

    private static ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<>();
    private static InheritableThreadLocal<Integer> inheritableThreadLocal =
            new InheritableThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {

        integerThreadLocal.set(1001); // father
        inheritableThreadLocal.set(1002); // father

        new Thread(() -> System.out.println(Thread.currentThread().getName() + ":"
                + integerThreadLocal.get() + "/"
                + inheritableThreadLocal.get())).start();

    }
}

//output Thread-0:null/1002

InheritableThreadLocal继承至Threadlocal,重写了childValue、getMap、createMap方法

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

可以看出InheritableThreadLocal使用的Map是Thread中inheritableThreadLocals变量!我们看下在Thread初始化过程中会对该变量进行的操作

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        ......
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
       ......
    }

    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }
//大概就是将父线程的ThreadLocalMap复制到自己的ThreadLocalMap里面来,这样我们就可以使用InheritableThreadLocal访问到父线程中的变量了
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

ThreadLocal使用例子

ThreadLocal实现的可复用的耗时统计工具Profiler

/**
 * @author perist
 * @date 2016/12/6
 * @time 21:36
 */
public class Profiler {

    private static final ThreadLocal<Long> durationThreadLocal = new ThreadLocal<Long>() {
        @Override
        protected Long initialValue() {
            return System.currentTimeMillis();
        }
    };

    public static void begin() {
        durationThreadLocal.set(System.currentTimeMillis());
    }

    public static long end() {
        return System.currentTimeMillis() - durationThreadLocal.get();
    }

    public static void main(String[] args) {
        Profiler.begin();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Profiler.end());
    }

}

ThreadLocal实现数据库连接线程隔离

/**
 * @author perist
 * @date 2016/12/6
 * @time 23:17
 */
public class ConnectionManager {

    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
        @Override
        protected Connection initialValue() {
            Connection conn = null;
            try {
                conn = DriverManager.getConnection(
                        "jdbc:mysql://localhost:3306/test", "username",
                        "password");
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return conn;
        }
    };

    public static Connection getConnection() {
        return connectionHolder.get();
    }

    public static void setConnection(Connection conn) {
        connectionHolder.set(conn);
    }
}

通过调用ConnectionManager.getConnection()方法,每个线程获取到的,都是和当前线程绑定的那个Connection对象,第一次获取时,是通过initialValue()方法的返回值来设置值的。通过ConnectionManager.setConnection(Connection conn)方法设置的Connection对象,也只会和当前线程绑定。这样就实现了Connection对象在多个线程中的完全隔离。

在Spring容器中管理多线程环境下的Connection对象时,采用的思路和以上代码非常相似。

关于GC

ThreadLocalMap对象保存在Thread对象中,当某个线程终止后,存储在其中的线程隔离的变量,也将作为Thread实例的垃圾被回收掉。所以不用担心内存泄漏的问题。

Thread的exit()方法做了大量帮助GC的操作:

    /**
     * This method is called by the system to give a Thread
     * a chance to clean up before it actually exits.
     */
    private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

ThreadLocal缺陷

ThreadLocal变量的这种隔离策略,也不是任何情况下都能使用的。

如果多个线程并发访问的对象实例只允许,也只能创建那么一个(单例),那就没有别的办法了,老老实实的使用同步机制吧。

0 0 投票数
Article Rating
订阅评论
提醒
guest
0 评论
最旧
最新 最多投票
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x