源码解读(一): ThreadLocal工作原理

ThreadLocal为解决并发问题提供了新的思路,能够简洁的编写出优美的多线程程序.

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本.

从线程的角度看,目标变量就象是线程的本地变量,这也是类名中Local所要表达的意思.所以,在Java中编写线程局部变量的代码相对来说要笨拙一些.

ThreadLocal中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

public class DataSourceTypeManager {
private static final ThreadLocal<DataSources> dataSourceTypes = new ThreadLocal<DataSources>() {
@Override
protected DataSources initialValue() {
return DataSources.DATASOURCE_DEFAULT;
}
};

public static DataSources get() {
return dataSourceTypes.get();
}

public static void set(DataSources dataSourceType) {
dataSourceTypes.set(dataSourceType);
}

public static void reset() {
dataSourceTypes.set(DataSources.DATASOURCE_DEFAULT);
}
}

initialValue(): 返回该线程局部变量的初始值,该方法是protected修饰的,是让子类去进行覆盖的。它是延迟调用的,在线程第1次调用get()set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

set(Object value): 设置当前线程的线程局部变量的值。

get(): 返回当前线程所对应的线程局部变量。

remove(): 将当前线程局部变量的值删除,目的是为了减少内存的占用,JDK 5.0新增方法。注意,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但可以加快内存回收的速度。

reset():


在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>
,新版本方法 void set(T value)T get()T initialValue()

如何做到每一个线程维护一个变量副本

在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。

源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//get方法实现
public T get() {
Thread t = Thread.currentThread(); //<1>
ThreadLocalMap map = getMap(t); //<2>
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//<3>
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
  • <1>取得当前线程t
  • <2>通过getMap(t)方法获取到一个map,类型为ThreadLocalMap
  • <3>接着通过当前对象作为key获取到<key,value>键值对,注意这里获取键值对传进去的是this,而不是当前线程t。
  • 如果获取成功,则返回value值。
  • 如果map为空,则调用setInitialValue方法返回value。

我们上面的每一句来仔细分析,首先看一下getMap方法中做了什么

1
2
3
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

getMap中,是调用当前线程t,返回当前线程t中的一个成员变量threadLocals
点进去这个方法,去Thread类中取看一下成员变量threadLocals是什么?

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

意思是,这个ThreadLocal变量是附属在这个线程中的,这个map是由ThreadLocal去维护的。ThreadLocalMap这个类是ThreadLocal类的一个内部类,继续看ThreadLocalMap的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
static class ThreadLocalMap {

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

//...略

可以看到ThreadLocalMapEntry继承了WeakReference,并且使用ThreadLocal作为Key。
继续看setInitialValue方法的具体实现:

1
2
3
4
5
6
7
8
9
10
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;
}

根据源码我们可以看到,就是如果map不为空,就设置键值对,为空,再创建Map,看一下createMap的实现:

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

如果为空,直接new了一个ThreadLocalMap,key是ThreadLocal对象


至此,已经明白了ThreadLocal是如何为每个线程创建变量的副本的:

  • 首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值(key)为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
  • 初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals
  • 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

代码实战

下面通过一个例子来证明通过ThreadLocal能达到在每个线程中创建变量副本的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Demo3 {
private ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
private ThreadLocal<String> stringLocal = new ThreadLocal<String>();


public void set() {
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}

public long getLong() {
return longLocal.get();
}

public String getString() {
return stringLocal.get();
}

public static void main(String[] args) throws InterruptedException {
final Demo3 test = new Demo3();


test.set();
System.out.println(test.getLong());
System.out.println(test.getString());


Thread thread1 = new Thread(){
public void run() {
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
};
};
thread1.start();
thread1.join();

System.out.println(test.getLong());
System.out.println(test.getString());
}
}

控制台输出:

1
2
3
4
5
6
1       //主线程id
main //主线程名称
10 //thread1 id
Thread-0 //thread1名称
1 //主线程id
main //主线程名称

从这段代码的输出结果可以看出,在main线程中和thread1线程中,longLocal保存的副本值和stringLocal保存的副本值都不一样。
最后一次在main线程再次打印副本值是为了证明在main线程中和thread1线程中的副本值确实是不同的。(因为当初他们设置值得时候就不会一样的)

 

  

总结

1)实际上的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;

2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象?

因为每个线程中可有多个threadLocal变量,就像上面代码实战1中的longLocalstringLocal

3)在进行get之前,必须先set,否则会报空指针异常;

如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。

如代码实战1,我们把set()方法注释掉,运行代码,就会报空指针

1
2
3
4
5
6
//test.set();

output:
Exception in thread "main" java.lang.NullPointerException
at com.may.threadlocal.Demo3.getLong(Demo3.java:14)
at com.may.threadlocal.Demo3.main(Demo3.java:26)

但是如果重写了initialValue方法后:

1
2
3
4
5
6
7
8
9
10
11
12
13
private ThreadLocal<Long> longLocal = new ThreadLocal<Long>(){
protected Long initialValue() {
return Thread.currentThread().getId();

};
};

private ThreadLocal<String> stringLocal = new ThreadLocal<String>(){
protected String initialValue() {
return Thread.currentThread().getName();
};

};

再去运行,可以正常运行,这样就可以直接不用先set而直接调用get了。建议,一般应用时,重写initialValue()方法,这个方法是延迟调用的,再次强调


ThreadLocal的应用场景

ThreadLocal的应用场合,我觉得最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。

最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。

代码1

1
2
3
4
5
6
7
8
9
private static ThreadLocal<Connection> connectionHolder= new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};

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

hibernate中典型的ThreadLocal的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static final ThreadLocal threadSession = new ThreadLocal();

public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
代码3

多数据源配置时使用(传送门)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DataSourceTypeManager {
private static final ThreadLocal<DataSources> dataSourceTypes = new ThreadLocal<DataSources>() {
@Override
protected DataSources initialValue() {
return DataSources.DATASOURCE_DEFAULT;
}
};

public static DataSources get() {
return dataSourceTypes.get();
}

public static void set(DataSources dataSourceType) {
dataSourceTypes.set(dataSourceType);
}

public static void reset() {
dataSourceTypes.set(DataSources.DATASOURCE_DEFAULT);
}
}

结束语

总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。归纳了两点:
1。每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
2。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。

参考:

END   

文章目录
  1. 1. ThreadLocal中的方法
  2. 2. 如何做到每一个线程维护一个变量副本
    1. 2.1. 源码
  3. 3. 代码实战
  •   
    1. 1. 总结
    2. 2. ThreadLocal的应用场景
      1. 2.1. 代码2
      2. 2.2. 代码3
    3. 3. 结束语
  • |