Java多线程(10)——ThreadLocal

来源:互联网 发布:第八届云计算大会ppt 编辑:程序博客网 时间:2024/05/21 06:17

ThreadLocal是Java框架中经常使用的工具。对于这个知识点,网上博文毛毛多,但有不少都存在一些错误。不过知识就是这样不断建立,发现问题,打破重建螺旋上升的过程。在此记录一下我的认识过程:

第一层级:初识

ThreadLocal顾名思义,线程局部变量。

因此ThreadLocal是线程独占而非处理多线程同步问题的。这在一些博文中有误解。

ThreadLocal就是在使用该对象的没一个线程中创建独立的副本,多个线程彼此之间是隔离的,实际上操作的是不同对象。

示例

代码

public class ThreadLocalDemo{     private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>(){        public Integer initialValue(){            return 0;        }    };    public int getNextNum(){        seqNum.set(seqNum.get() + 1);        return seqNum.get();    }    public static void main(String[] args){        ThreadLocalDemo sn = new ThreadLocalDemo();        TestClient t1 = new TestClient(sn);        TestClient t2 = new TestClient(sn);        TestClient t3 = new TestClient(sn);        TestClient t4 = new TestClient(sn);        t1.start();        t2.start();        t3.start();        t4.start();    }    private static class TestClient extends Thread {        private ThreadLocalDemo sn;        public TestClient(ThreadLocalDemo sn){            this.sn = sn;        }        public void run(){            for(int i=0;i<3;i++){                System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn[" + sn.getNextNum() + "]");            }        }    }}

输出结果:

thread[Thread-0] --> sn[1]thread[Thread-0] --> sn[2]thread[Thread-2] --> sn[1]thread[Thread-1] --> sn[1]thread[Thread-1] --> sn[2]thread[Thread-1] --> sn[3]thread[Thread-3] --> sn[1]thread[Thread-3] --> sn[2]thread[Thread-2] --> sn[2]thread[Thread-0] --> sn[3]thread[Thread-2] --> sn[3]thread[Thread-3] --> sn[3]

多提一句,可以发现代码没有引入任何其他类库,原来ThreadLocal和Thread都是java.lang包下的类,已经自动加载了。

源码分析

ThreadLocal的实现在java.lang.ThreadLocal类中;
ThreadLocal有一个内部类ThreadLocalMap;
Thread类中有一个ThreadLoacl.ThreadLocalMap域threadLocals。

很多博文混淆不清就是因为不能理清楚ThreadLocal,ThreadLocalMap,Thread之间的关系。

ThreadLocal

1,ThreadLocal是个泛型类:

public class ThreadLocal<T> {

2,ThreadLocal的域:

ThreadLocal只有三个域,

private final int threadLocalHashCode = nextHashCode();private static AtomicInteger nextHashCode =        new AtomicInteger();private static final int HASH_INCREMENT = 0x61c88647;

配合一个方法:

private static int nextHashCode() {    return nextHashCode.getAndAdd(HASH_INCREMENT);}

做的事情就是实现一个ThreadLocal的哈希值threadLocalHashCode,这个哈希值在ThreadLocalMap中用到。

3,ThreadLocal的方法:

Thread一共四个基本方法:

(1) void set(Object value)设置当前线程的线程局部变量的值。 (2) public Object get()该方法返回当前线程所对应的线程局部变量。 (3) public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。 (4) protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次,ThreadLocal中的缺省实现直接返回一个null

基本方法都是通过封装ThreadLocalMap的方法实现的,在看过了ThreadLocal源码后更好理解。

ThreadLocalMap

1, ThreadLocalMap没有继承任何Map,而是单独实现了一个Map功能。如果理解HashMap源码的话再看ThreadLocalMap会比较轻松。

2, ThreadLocalMap元素存储:

ThreadLocalMap定义了一个内部类Entry来存储元素:

static class Entry extends WeakReference<ThreadLocal<?>> {    /** The value associated with this ThreadLocal. */    Object value;    Entry(ThreadLocal<?> k, Object v) {        super(k);        value = v;    }}

Entry继承了 WeakReference,实现一个键值对,键是ThreadLocal,值是一个Object。

有了Entry后,ThreadLocalMap持有一个Entry数组table来存储元素,和HashMap类似。

private Entry[] table;

3,ThreadLocalMap元素操作:

作为一个Map,自然要有get,set操作,在这里就用到了上面ThreadLocal中说到的threadLocalHashCode。以get为例:

private Entry getEntry(ThreadLocal<?> key) {    int i = key.threadLocalHashCode & (table.length - 1);    Entry e = table[i];    if (e != null && e.get() == key)        return e;    else        return getEntryAfterMiss(key, i, e);}

可知ThreadLocalMap是使用threadLocalHashCode来做元素定位的。

Thread

Thread类和ThreadLocal相关的就是它的一个域:

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

源码已经说明了,这个域的管理是ThreadLocal来做的,而Thread不操作它,唯一与之发生关系的就是线程退出方法exit():

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

把threadLocals置为空,这在下节的内存泄露问题还将提到。

再看ThreadLocal

之前我们看过Thread的大框知道了ThreadLocal的四个基本方法,也说明了基本方法是调用ThreadLocalMap实现的,那么接下来看如何实现的:

首先看获取和创建ThreadLocalMap的方法:

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

可知,是根据线程,获取线程的threadLocals域,但threadLocals域默认为空,所以有了创建方法createMap:

    /**     * Create the map associated with a ThreadLocal. Overridden in     * InheritableThreadLocal.     *     * @param t the current thread     * @param firstValue value for the initial entry of the map     */    void createMap(Thread t, T firstValue) {        t.threadLocals = new ThreadLocalMap(this, firstValue);    }

创建方法,发现在创建同时还放入了一个元素firstValue,由此可以推测,基本方法应该要利用getMap和createMap配合条件来实现的,让我们一探究竟:

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();    }
    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,再根据ThreadLocal本身操作对象的过程。

总结:
一个ThreadLocal实例一方面本身代表着一个键,代表着一个指定类型<T>的变量,另一方面拥有着根据键,操作线程存储和获取这个变量的方法。

每个线程Thread都拥有一个ThreadLocalMap表,里面可以以键值对形式存不同类型的变量,其中键是ThreadLocal类型,值是ThreadLocal<T>的T类型。

概念之所以比较容易混淆,是因为在ThreadLocal中放了太多的东西,如果我来实现,将ThreadLocal和ThreadLocalMap分离,再将ThreadLocal的类型定义功能和线程操作功能分开,会更好理解。但写在一起可以做到更好的封装,应该会有利于安全性吧。

内存泄露问题

在上面提到过,每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap。Map中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key。 每个key都弱引用指向threadlocal。当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。

但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用。只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread,Map, value将全部被GC回收。

所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露,这时就要根据要实现的功能妥善处理了。

在深入

看似理解,但纸上得来终觉浅,遇到问题还是会发现模棱两可。对于遇到的问题,记录如下,适时更新。

1,ThreadLocalMap存储的时间键值对,那么键是什么,值是什么?

答:键是ThreadLocal实例,值是对应线程的变量副本。
详细说明:ThreadLocalMap把ThreadLocal实例作为键,而在实际方法中,是获取ThreadLocal的哈希值threadLocalHashCode。在复制一次源码:

private final int threadLocalHashCode = nextHashCode();private static AtomicInteger nextHashCode =        new AtomicInteger();private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() {    return nextHashCode.getAndAdd(HASH_INCREMENT);}

仔细分析,threadLocalHashCode是final的,在ThreadLocal实例初始化是通过方法nextHashCode()计算得来,初始化后不可变。

而nextHashCode是static,是静态变量,再看nextHashCode()方法,也是static的,操作nextHashCode自增固定长度。所以每多一个ThreadLocal实例,nextHashCode就会增加,保证了每两个ThreadLocal之间的threadLocalHashCode都不样,这样把它作为键值对的键就可行了。

思考一下threadLocalHashCode有没有可能重复,答案是很难,当ThreadLocal实例很多,可能超过int值范围,这样可能会转回来,使两个哈希值相同。但这样的情况微乎其微,首先这需要有相当多的ThreadLocal实例,其次每次递增的步长HASH_INCREMENT可能也是有讲究的(我猜测,不确定)。所以此事不足为虑。

原创粉丝点击