Java并发编程系列(六)---- ThreadLocal使用及源码分析

ThreadLocal成为线程本地变量,从名字大概可以猜出是和线程本地变量有关的,提供的方法主要有这么几个

//获取值
public T get() { }
//设置值
public void set(T value) { }
//移除
public void remove() { }
//初始化值
protected T initialValue() { }

ThreadLocal的使用

我们看看ThreadLocal怎么用:

package com.rancho945.concurrent;


public class Test {

    public static void main(String[] args) {
        final ThreadLocal<String> local = new ThreadLocal<String>();
        //注意这里是在主线程里设置的
        local.set("hello world");
        new Thread(new Runnable() {

            @Override
            public void run() {
                //我们看看能不能在另外的线程获取到主线程设置的local值
                System.out.println("1."+Thread.currentThread().getName()+"---"+local.get());
                //然后新线程设置值
                local.set("hello ThreadLocal");
                //获取并输出输出
                System.out.println("2."+Thread.currentThread().getName()+"---"+local.get());
                //移除
                local.remove();
                //输出
                System.out.println("3."+Thread.currentThread().getName()+"---"+local.get());
            }
        }).start();
        //保证子线程执行完成
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //主线程获取并输出
        System.out.println("4."+Thread.currentThread().getName()+"---"+local.get());
    }

}

执行结果

1.Thread-0---null
2.Thread-0---hello ThreadLocal
3.Thread-0---null
4.main---hello world

可以看到,虽然local实例是两个线程共享的,但是他们之间设值以及取值是不会相互影响的,这就是本地线程变量。
我们在看看initialValue是怎么用的,把上面的例子中的

final ThreadLocal<String> local = new ThreadLocal<String>();

改为

final ThreadLocal<String> local = new ThreadLocal<String>(){
            @Override
            protected String initialValue() {
                // TODO Auto-generated method stub
                return "hehe";
            }
        };

那么执行结果为:

1.Thread-0---hehe
2.Thread-0---hello ThreadLocal
3.Thread-0---hehe
4.main---hello world

可以看到,如果重写了initialValue,那么当线程没有设置值或者已经移除了,那么会将initialValue返回值作为默认值。

源码分析

ThreadLocal类里面有个内部类ThreadLocalMap,负责ThreadLocal与线程本地变量之间的关系,Thread类有一个ThreadLocalMap类的成员变量。

看看ThreadLocal类的成员变量

//每个Thread都有一个哈希码
private final int threadLocalHashCode = nextHashCode();
//用于获取下一个哈希码
private static AtomicInteger nextHashCode = new AtomicInteger();
//哈希的增长,这个是一个非常神奇的黄金数字,可以使哈希结果非常均匀地分布在2^N的空间里,具体实现可以搜索一下,不多说。
private static final int HASH_INCREMENT = 0x61c88647;
//获取哈希码永远都是自增长0x61c88647,一个非常神奇的数字。
private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

set(T value)方法

看看ThreadLocal类的set方法:

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取线程的TheadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

在Thread中:

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap为ThreadLocal的一个静态内部类,初始时获取到线程的map为null,也就是线程的threadLocals是延迟初始化的,线程没有用到ThreadLocal功能的时候就不创建对象,避免冗余,充分体现了JDK大神深厚的编码功底。我们先看看createMap的实现:

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

这里是创建当前线程的threadLocals变量,注意这里传进去的第一个参数是this,也就是当前的threadlocal实例对象。看看ThreadLocalMap内部类

 /**
 * The initial capacity -- MUST be a power of two.
 */
 //这里是初始化容量
private static final int INITIAL_CAPACITY = 16;

/**
 * The table, resized as necessary.
 * table.length MUST always be a power of two.
 */
 //这里是保存键值对的对象数组,Entry负责保存一个键值对,表示threadloacl对象实例和值的关系
private Entry[] table;

/**
 * The number of entries in the table.
 */
 //表示当前的规模的大小
private int size = 0;

/**
 * The next size value at which to resize.
 */
 //这里是需要重新扩充table的时size上限大小
private int threshold; // Default to 0

//这里是真正保存ThreadLocal实例对象和值的地方,用了一个弱引用来引用ThreadLocal实例对象
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //tabale初始化的,大小为INITIAL_CAPACITY
    table = new Entry[INITIAL_CAPACITY];
    //这个就是根据前面很神奇的0x61c88647有关,这里算出的值可以使多个threadlocal均匀分布在数组中,
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //在数组i的位置存放threadlocal与线程本地变量的k-v关系对象
    table[i] = new Entry(firstKey, firstValue);
    //设置当前大小
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

private void setThreshold(int len) {
    //这里设置重新调整size为len的三分之二
    //比如初始化的时候为16,那么16*2/3=10就要对table扩容
    threshold = len * 2 / 3;
}

这里的主要工作就是给当前线程创建ThreadLocalMap实例,把当前threadlocal对象和需要放进去的值关系储存在ThreadLocalMap实例的table中。如果说一个线程用了三个threadlocal对象,比如:

final ThreadLocal<String> local1 = new ThreadLocal<String>();
final ThreadLocal<String> local2 = new ThreadLocal<String>();
final ThreadLocal<String> local3 = new ThreadLocal<String>();
new Thread(new Runnable() {

    @Override
    public void run() {
        local1.set("ThreadLocal1");
        local2.set("ThreadLocal2");
        local3.set("ThreadLocal3");
    }
}).start();

那么local1和”ThreadLocal1”作为一个entry放在线程threadLocals(ThreadlocalMap实例)变量的table中,local2和”ThreadLocal2”作为一个entry也放该table中,local2和”ThreadLocal2”作为一个entry也放在table中。当然后面两个entry不是在create方法中放进去的,以为第一次create后,getMap方法就已经不在返回null,而是执行map.set()

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取线程的TheadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

看看map.set():

/**
 * Set the value associated with key.
 *
 * @param key the thread local object
 * @param value the value to be set
 */
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    //这里从table的i处开始查找,直到找到entry为空
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        //如果找到的key和要设置的key相同,则证明是同一个threadlocal,把它的值替换一下,返回
        if (k == key) {
            e.value = value;
            return;
        }
        //如果找到的key为null,证明已经被回收,那么久把该清除并用当前的k-v替换,然后返回,具体替换过程不再仔细分析
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    //如果没有在table中找到,那么在i的位置新增一个节点。
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //这里也是清除一些被回收的Threadlocal对象在table中的位置,并且判断是否应该扩容。
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}


private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

get()方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //使用当前threadlocal获取线程ThreadLocalMap中的entry
        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();
    //下面的分析和前面set方法一样
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
//如果新建ThreadLocal的时候没有重写,则默认返回空
protected T initialValue() {
    return null;
}

这也就解释了前面为什么没有set之前或者remove之后会返回的是initialValue方法的返回值。

remove()方法

ThreadLocal中的remove方法:

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

可以看到调用的是ThreadLocalMap的方法:

 /**
 * Remove the entry for key.
 */
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    //找到entry
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
         //如果找到了,则清除
        if (e.get() == key) {
            //这里清除entry的对threadlocal对象的引用
            e.clear();
            //这里清除的是table中i位置的entry
            expungeStaleEntry(i);
            return;
        }
    }
}

思考

看看下面的代码执行会输出什么

package com.rancho945.concurrent;

public class Counter {
    public int count = 0 ;

    public void increment(){
        count++;
    }
}
package com.rancho945.concurrent;


public class Test {

    public static void main(String[] args) {
        final ThreadLocal<Counter> local = new ThreadLocal<Counter>();
        final Counter counter = new Counter();
        //注意这里是在主线程里设置的,默认count值为0
        local.set(counter);
        //在主线程中自增
        counter.increment();
        new Thread(new Runnable() {

            @Override
            public void run() {

                local.set(counter);
                //子线程从同一个local中获取count
                System.out.println(local.get().count);
            }
        }).start();

    }
}

想一下,执行结果是什么?是0吗?答案是1。这和开头的例子怎么不一样啊?难道用的是假的ThreadLocal?说好的不同线程之间相互不影响呢?

我们看这里和开头的有什么不同,开头的例子主线程和子线程set的对象是不同的,而这里set的却是同一个对象。

也就是说,threadlocal设置不同的对象线程之间相互不影响,如果是同一个对象,那么仍然还是会相互影响。线程副本的意思并不是说set同一个对象,能够把该对象复制一份。实质上仍然是同一个对象。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值