一、何谓“ThreadLocal”
* ThreadLocal: 特别好的完成的封闭线程 ThreadLocal提供线程级别的变量.这些变量不同于它们正常下的变量副本, * 在每一个线程中都有它自己获取方式(通过它的get和set方法),不依赖变量副本的初始化。 * 它的实例通常都是私有的静态的,用于关联线程的上下文。 * 这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量 * * 总结:ThreadLocal的作用是提供线程内部的局部变量,这种变量只存在线程的生命周期。
声明方式:private static ThreadLocal<Object> threadLocal = new ThreadLocal<Object>;
public static ThreadLocal<List<String>> threadLocal = new ThreadLocal<>();
public void setThreadLocal(List<String> values) {
threadLocal.set(values);
}
public void getThreadLocal(){
threadLocal.get().forEach(name ->
System.out.println(Thread.currentThread().getName()+":"+name));
}
public static void main(String[] args) {
final ThreadLocalDemo threadLocal = new ThreadLocalDemo();
new Thread(() ->{
List<String> list = new ArrayList<>();
list.add("hu");
list.add("yun");
list.add("qiang");
threadLocal.setThreadLocal(list);
threadLocal.getThreadLocal();
}).start();
new Thread(() ->{
List<String> list = new ArrayList<>();
list.add("nihoa");
list.add("buhao");
threadLocal.setThreadLocal(list);
threadLocal.getThreadLocal();
}).start();
}
}
看出虽然多个线程对同一个变量进行访问,但是由于threadLocal
变量由ThreadLocal
修饰,则不同的线程访问的就是该线程设置的值,这里也就体现出来ThreadLocal的作用
Thread-0:hu
Thread-1:nihoa
Thread-1:buhao
Thread-0:yun
Thread-0:qiang
二,工作原理
从上面的源码分析,我们可以得出ThreadLocal
的工作原理如下
-
声明全局的
ThreadLocal
变量,private static ThreadLocal<Object> threadLocal = new ThreadLocal<Object>;
-
每个线程中都有属于自己的
ThreadLocalMap
,互不干扰 -
全局只有一个
threadLocal
,当通过set
填充数据时,通过获取当前操作线程的threadLocalMap
,将threadLocal
作为threadLocalMap
中的key
,需要填充的值作为value
-
当需要从
threadLocal
获取值时,通过获取当前操作线程的threadLocalMap
,并返回key
为threadLocal
对象的value
那么就可以理解为:`ThreadLocal`的活动范围是具体的某一个线程,并且是该线程独有的。它不是用来解决共享变量的多线程安全问题。
但是,有一点需要说明的是,如果`ThreadLocal`通过`set`方法放进去的值,这个值是共享对象,那么还是会存在线程安全问题。
如何保证两个同时实例化的
ThreadLocal
对象有不同的threadLocalHashCode
属性:在ThreadLocal
类中,还包含了一个static修饰的AtomicInteger
([əˈtɒmɪk]提供原子操作的Integer类)成员变量(即类变量)和一个static final 修饰的常量(作为两个相邻nextHashCode
的差值)。由于nextHashCode
是类变量,所以每一次调用ThreadLocal
类都可以保证nextHashCode
被更新到新的值,并且下一次调用ThreadLocal
类这个被更新的值仍然可用,同时AtomicInteger
保证了nextHashCode
自增的原子性。
ThreadLocal
中的ThreadLocalMap
中的key
为ThreadLocal
对象,由于每个实例化的ThreadLocal
对象都是不相同的,所以不会存在key
冲突,所以一个线程存在多个ThreadLocal
对象作为key
是完全没有问题的。也就是说,一个线程中的ThreadLocalMap
可以存在多个key
。
为什么使用ThreadLocal
作为ThreadLocalMap
的key
? 上面的解析已经很明确了。
试试使用线程id
作为ThreadLocalMap
的key
? 如果使用线程id
作为key
,如果存在两个ThreadLocal
对象,一个存放String
类型,另一个存放Integer
类型,而在单个线程中只存在一个ThreadLocalMap
,当存放数据时,key
永远只会有一个(线程id),存入数据的时候先存会被后存覆盖,获取数据时候可能会发生错误。
三、ThreadLocal的用法
应用场景
ThreadLocal中存放的变量只在线程的生命周期内起作用,应用场景只要有两个方面:
- 提供一个线程内公共变量(比如本次请求的用户信息、实体参数),减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度
- 为线程提供一个私有的变量副本,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
三、ThreadLocal的源码分析
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
在这个方法内部我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。
==线程隔离的秘密,就在于ThreadLocalMap这个类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。== 这个就是实现原理
源码实现片段:getMap、createMap
/**
* 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;
}
/**
* 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
* @param map the map to store.
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
源码实现片段:get
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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();
}
源码实现片段:setInitialValue
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
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;
}
//获取和当前线程绑定的值时,ThreadLocalMap对象是以this指向的ThreadLocal对象为键
//进行查找的,这当然和前面set()方法的代码是相呼应的。进一步地,我们可以创建不同的
//ThreadLocal实例来实现多个变量在不同线程间的访问隔离,为什么可以这么做?因为不
//同的ThreadLocal对象作为不同键,当然也可以在线程的ThreadLocalMap对象中设置不同
//的值了。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个
//HashMap对象中存储一个键值对和多个键值对一样,仅此而已。
四、ThreadLocal实际用途
例1:项目中动态数据源的设定
/**
* 动态数据源
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
public static void setDataSource(String dataSource) {
contextHolder.set(dataSource);
}
public static String getDataSource() {
return contextHolder.get();
}
public static void clearDataSource() {
contextHolder.remove();
}
}
在单线程的情况下这样写并没有问题,但如果在多线程情况下回出现线程安全的问题。你可能会说用同步关键字或锁来保障线程安全,这样做当然是可行的,但考虑到性能的问题所以这样子做并是很优雅。
下面是改造后的代码:
public class ConnectionManager {
private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
public static Connection getConnection() {
if(connThreadLocal.get() != null)
return connThreadLocal.get();
//获取一个连接并设置到当前线程变量中
Connection conn = getConnection();
connThreadLocal.set(conn);
return conn;
}
...
}
由于在方法中需要频繁地开启和关闭数据库连接,这样不仅严重影响程序执行效率,还可能导致服务器压力巨大。我们使用ThreadLocal创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,当一个
数据源获取连接后通过ThreadLocal的 set方法将获取的连接获取到当前线程的变量中
例2:日期格式(摘自网上)
使用这个日期格式类主要作用就是将枚举对象转成Map而map的值则是使用ThreadLocal存储,那么在实际的开发中可以在同一线程中的不同方法中使用日期格式而无需在创建日期格式的实例。
public class DateFormatFactory {
public enum DatePattern {
TimePattern("yyyy-MM-dd HH:mm:ss"),
DatePattern("yyyy-MM-dd");
public String pattern;
private DatePattern(String pattern) {
this.pattern = pattern;
}
}
private static final Map<DatePattern, ThreadLocal<DateFormat>> pattern2ThreadLocal;
static {
DatePattern[] patterns = DatePattern.values();
int len = patterns.length;
pattern2ThreadLocal = new HashMap<DatePattern, ThreadLocal<DateFormat>>(len);
for (int i = 0; i < len; i++) {
DatePattern datePattern = patterns[i];
final String pattern = datePattern.pattern;
pattern2ThreadLocal.put(datePattern, new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat(pattern);
}
});
}
}
//获取DateFormat
public static DateFormat getDateFormat(DatePattern pattern) {
ThreadLocal<DateFormat> threadDateFormat = pattern2ThreadLocal.get(pattern);
//不需要判断threadDateFormat是否为空
return threadDateFormat.get();
}
public static void main(String[] args) {
String dateStr = DateFormatFactory.getDateFormat(DatePattern.TimePattern).format(new Date());
System.out.println(dateStr);
}
}
五、总结
ThreadLocal是用冗余的方式换时间,而锁机制则是时间换空间,好的设计往往都是在时间、空间以及复杂度之间做权衡,道理是这样但是真正能平衡三者之间的人我姑且称之为“大成者”,愿你我在成长的道路上越走越远。