代码改变世界

ThreadLocal

2012-12-14 17:10  ggzwtj  阅读(1853)  评论(2编辑  收藏  举报

ThreadLocal和Thread的关系如下:

这个结构还是非常清楚的,但是看网上的一些代码感觉很晕,下面来看为什么会晕,比如(下面的代码是根据网上的代码重写的):

class MyThreadLocal extends ThreadLocal<Integer> {
    public Integer initialValue() {
        return 0;
    }

    public void incr() {
        Integer value = get();
        if (value == null) {
            value = 0;
        }
        set(value + 1);
    }
}
class MyThread extends Thread {
    MyThreadLocal value = null;

    MyThread(MyThreadLocal value) {
        this.value = value;
    }

    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread() + " " + value.get());
            value.incr();
        }
    }
}

public class A {
    public static void main(String[] args) {
        MyThreadLocal onlyOne = new MyThreadLocal();
        Thread a = new MyThread(onlyOne);
        Thread b = new MyThread(onlyOne);
        a.start();
        b.start();
    }
}
-----------------------------------
output is:
Thread[Thread-1,5,main] 0
Thread[Thread-1,5,main] 1
Thread[Thread-1,5,main] 2
Thread[Thread-0,5,main] 0
Thread[Thread-0,5,main] 1
Thread[Thread-0,5,main] 2

在代码的最后给出了程序的输出,可以两个线程虽然是用的同一个MyThreadLocal,但是丝毫没有相互影响,为什么?

  1. 首先,我们看到MyThreadLocal中没有保存任何的数据,那么相互之间有没有影响当然也就和MyThreadLocal本身没有关系了!
  2. 然后,再看ThreadLocal中,算的上属性的也只有threadLcalHashCode,显然这只是一个在创建的的时候设置好的常量,所以也没关系!

没有找到,最直接的看下get方法不就知道在哪里保存了,如下:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}

那么可以看到,其实每次get的时候只是从Thread.threadLocals中去取值,而不管是MyThreadLocal还是ThreadLocal,只是对ThreadLocalMap的封装而已(所以被上面的代码绕晕了)。从代码ThreadLocalMap.Entry e = map.getEntry(this)可以知道,其实ThreadLocal是作为一个Key存在的,也就是外面看到的是Key,那么两个线程在能看到同一个ThreadLocal的时候并试图通过它来操作的时候会发现:

根据同一个ThreadLocal在不同的两个线程中get到的结果是不一样的(当然也有可能是一样)。而如果ThreadB错误地调用Thread.set(ObjectB)的时候,这只会在ThreadLocalMapB中保存一条垃圾数据,而不会影响到ThreadA。那么,如果我们自己不用ThreadLocal的话,怎么来实现类似的功能呢?

class MyThread extends Thread {
    private Map value = null;
    public void run() {
         // do someting
    }
}

把线程要用到的数据保存在value中,那么其他的线程就访问不到了(也就线程安全了),但是如果在多考虑一些东西,其实写完和ThreadLocal和ThreadLocalMap的代码差不多了。好了,下面来看ThreadLocalMap的代码:

private void set(ThreadLocal key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal k = e.get();
        if (k == key) {
            e.value = value;
            return;
        }
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

在set的时候有几种种情况:

  1. 从HashCode的位置开始遍历Entry,如果找到Key,那么该位置设置为Value,返回;
  2. 在遍历的过程中发现Key=null的Entry、并且后面有Key相等的Entry,那么就将这两个位置交换并设置value值;
  3. 如果发下Key=null的Entry,但是后面没有相等Key的Entry,那么就Value设置到这个Entry;
  4. 上面的情况都没有发生,最后落在了Entry=null的情况,此时创建一个Entry(这里相当于table变大了,所以要检查是否需要resize); 

还有两个有意思的方法:

  1. cleanSomeSlots:本意是清楚一些废弃的Entry,但是其中的循环却只执行了Log(N)次,其实它是在寻找“效率”和“效果”之间的平衡点;
  2. expungeStaleEntry:清理垃圾和解决hash冲突,这样的话有助于让GC去回收内存;

有了垃圾清理的时候,如果ThreadLocal已经被释放了(被设置为NULL),那么就有一定的概率在普通的操作中将对应Entry设置为Null从而被回收,如果不回收的话是不是有可能产生内存泄露?

看到这里,其实ThreadLocal使得我们的线程能安全的访问一些变量(就算是我们把ThreadLocal暴露给了所有的线程),而能安全访问得益于ThreadLocalMap被保存在Thread类中,值得注意的地方是防止内存泄露,到这里就会发现其实ThreadLocal这个名字还是相当准确的。。。

---------------------------------------

欢迎拍砖。