您的位置:首页 > 编程语言 > Java开发

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

2017-02-11 00:56 507 查看
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同一个对象,能够把该对象复制一份。实质上仍然是同一个对象。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: