一、ThreadLocal(线程变量副本)介绍
Synchronized实现内存共享,ThreadLocal为每个线程维护一个本地变量。
采用空间换时间,它用于线程间的数据隔离,为每一个使用该变量的线程提供一个副本,每个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。
ThreadLocal类中维护一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值为对应线程的变量副本。
ThreadLocal在Spring中发挥着巨大的作用,在管理Request作用域中的Bean、事务管理、任务调度、AOP等模块都出现了它的身影。
Spring中绝大部分Bean都可以声明成Singleton作用域,采用ThreadLocal进行封装,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。
ThreadLocal从另一个角度来解决多线程的并发访问。
ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。ThreadLocal是解决线程安全问题一个很好的思路
它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发
二、ThreadLocal 实现原理
- 用于在同一个线程的上下文中传递数据,保证并发安全。
- 底层是一个 ThreadLocalMap,是 Thread 对象的一个实例字段,以 ThreadLocal 对象为键,存放的数据为值。键使用的是弱引用。
ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。
也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
三、ThreadLocal为什么会内存泄漏
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
但是这些被动的预防措施并不能保证不会内存泄漏:
使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏(参考ThreadLocal 内存泄露的实例分析)。
分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。
3.1 为什么使用弱引用
从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
我们先来看看官方文档的说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了应对非常大和长时间的用途,哈希表使用弱引用的 key。
3.2 下面我们分两种情况讨论:
key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
四、ThreadLocal 最佳实践
综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
序列号生成器-保证每个线程得到的序列号都是自增的,而不能相互干扰
4.1 多线程实现序列号生成器(相互干扰,未成功实现)
- 序列接口
每次调用 getNumber() 方法可获取一个序列号,下次再调用时,序列号会自增。
public interface Sequence {
int getNumber();
}
- 线程类
在线程中连续输出三次线程名与其对应的序列号。
public class ClientThread extends Thread {
private Sequence sequence;
public ClientThread(Sequence sequence) {
this.sequence = sequence;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " => "
+ sequence.getNumber());
}
}
}
我们先不用 ThreadLocal,来做一个实现类吧。
public static void main(String[] args) throws InterruptedException {
Sequence sequence = new SequenceA();
ClientThread thread1 = new ClientThread(sequence);
ClientThread thread2 = new ClientThread(sequence);
ClientThread thread3 = new ClientThread(sequence);
thread1.start();
/**获取控制权*/
thread1.join();
thread2.start();
thread2.join();
thread3.start();
}
输出结果
输出结果:
Thread-0 => 1
Thread-0 => 2
Thread-0 => 3
Thread-1 => 4
Thread-1 => 5
Thread-1 => 6
Thread-2 => 7
Thread-2 => 8
Thread-2 => 9
- 由于线程启动顺序是随机
所以并不是0、1、2这样的顺序,这个好理解。为什么当 Thread-0 输出了1、2、3之后,而 Thread-2 却输出了4、5、6呢?线程之间竟然共享了 static 变量!这就是所谓的“非线程安全”问题了。 - 那么如何来保证“线程安全”呢
对应于这个案例,就是说不同的线程可拥有自己的 static 变量,如何实现呢?下面看看另外一个实现吧。
4.2 threadlocal实现序列号生成器
public class SequenceB implements Sequence {
private static ThreadLocal<Integer> numberContainer = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public int getNumber() {
numberContainer.set(numberContainer.get() + 1);
return numberContainer.get();
}
public static void main(String[] args) throws InterruptedException {
Sequence sequence = new SequenceB();
ClientThread thread1 = new ClientThread(sequence);
ClientThread thread2 = new ClientThread(sequence);
ClientThread thread3 = new ClientThread(sequence);
thread1.start();
thread1.join();
thread2.start();
thread2.join();
thread3.start();
}
}
通过 ThreadLocal 封装了一个 Integer 类型的 numberContainer 静态成员变量,并且初始值是0。再看 getNumber() 方法,首先从 numberContainer 中 get 出当前的值,加1,随后 set 到 numberContainer 中,最后将 numberContainer 中 get 出当前的值并返回。
是不是很恶心?但是很强大!确实稍微饶了一下,我们不妨把 ThreadLocal 看成是一个容器,这样理解就简单了。所以,这里故意用 Container 这个单词作为后缀来命名 ThreadLocal 变量。运行结果如何呢?看看吧。
运行结果:
Thread-0 => 1
Thread-0 => 2
Thread-0 => 3
Thread-1 => 1
Thread-1 => 2
Thread-1 => 3
Thread-2 => 1
Thread-2 => 2
Thread-2 => 3
每个线程相互独立了,同样是 static 变量,对于不同的线程而言,它没有被共享,而是每个线程各一份,这样也就保证了线程安全。 也就是说,TheadLocal 为每一个线程提供了一个独立的副本!
搞清楚 ThreadLocal 的原理之后,有必要总结一下 ThreadLocal 的 API,其实很简单。
- public void set(T value):将值放入线程局部变量中
- public T get():从线程局部变量中获取值
- public void remove():从线程局部变量中移除值(有助于 JVM 垃圾回收)
- protected T initialValue():返回线程局部变量中的初始值(默认为 null)
为什么 initialValue() 方法是 protected 的呢?就是为了提醒程序员们,这个方法是要你们来实现的,请给这个线程局部变量一个初始值吧。
五、ThreadLocal实现读写分离
5.1 xml配置
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close" primary="true">
...
</bean>
<bean id="dataSourceBak" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close" >
...
</bean>
<bean id="multipleDataSource" class="com.doordu.soa.service.web.dao.MultipleDataSource">
<property name="defaultTargetDataSource" ref="dataSource"/>
<property name="targetDataSources">
<map>
<entry key="dataSource" value-ref="dataSource"/>
<entry key="dataSourceBak" value-ref="dataSourceBak"/>
</map>
</property>
</bean>
<!-- (事务管理)transaction manager, use JtaTransactionManager for global tx -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="multipleDataSource" />
</bean>
5.2 多数据库管理类
public class MultipleDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal<String>();
public static void setDataSourceKey(String dataSource) {
dataSourceKey.set(dataSource);
}
@Override
protected Object determineCurrentLookupKey() {
return dataSourceKey.get();
}
}
5.3 数据库切换切面
@Component
@Aspect
public class MultipleDataSourceAspectAdvice {
private static final Logger logger = Logger.getLogger(MultipleDataSourceAspectAdvice.class);
@Pointcut(value = "execution(public * com.doordu.soa.service.house.service..*(..))")
public void houseService() {
}
@Around(value = "houseService()")
public Object doValid(ProceedingJoinPoint joinPoint) throws Throwable {
if (joinPoint.getTarget() instanceof CityCodeServiceImpl) {
MultipleDataSource.setDataSourceKey("dataSourceBak");
}else if(joinPoint.getTarget() instanceof DistrictCodeServiceImpl){
MultipleDataSource.setDataSourceKey("dataSourceBak");
}else if(joinPoint.getTarget() instanceof ProvinceCodeServiceImpl){
MultipleDataSource.setDataSourceKey("dataSourceBak");
}else if(joinPoint.getTarget() instanceof UtEstateServiceImpl){
MultipleDataSource.setDataSourceKey("dataSourceBak");
}
else{
MultipleDataSource.setDataSourceKey("dataSource");
}
return joinPoint.proceed();
}
}