什么是ThreadLocal?
使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。从线程的角度看,目标变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。
ThreadLocal的作用
线程并发:在多线程并发的场景下
传递数据:通过ThreadLocal在同一线程,不同组件传递公共变量。
线程隔离:每个线程的变量都是独立的,不会相互影响
ThreadLocal常用的方法
方法申明 | 描述 |
ThreadLocal() | 创建ThreadLocal对象 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
案例演示:
ThreadLocal方法
public class ThreadDemo {
ThreadLocal<String> t = new ThreadLocal<>();
private String content;
public void setContent(String content) {
t.set(content);
}
public String getContent() {
return t.get();
}
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
for (int i = 0; i <5 ; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
threadDemo.setContent(Thread.currentThread().getName()+"的数据");
System.out.println("--------------------");
System.out.println( Thread.currentThread().getName()+"线程"+"--->"+threadDemo.getContent());
}
});
thread.start();
}
}
}
synchronized方法:
public class ThreadDemo {
private String content;
public void setContent(String content) {
this.content = content;
}
public String getContent() {
return content;
}
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
for (int i = 0; i <5 ; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (ThreadDemo.class){
threadDemo.setContent(Thread.currentThread().getName()+"的数据");
System.out.println("--------------------");
System.out.println( Thread.currentThread().getName()+"线程"+"--->"+threadDemo.getContent());
}
}
});
thread.start();
}
}
}
虽然说 synchronized也能解决这个问题,但是,在这里我们侧重解决的是线程的数据隔离问题,而不是共享变量的问题。所以使用 synchronized不太合适。
ThreadLocal与Synchronized的区别
synchronized | ThreadLocal | |
原理 | 同步机制,采用时间换空间的方式,让不同的线程排队等候。 | 以空间换时间的方式,给每个线程都给了一个变量副本,从而实现同时访问,线程之间互不干扰。 |
侧重点 | 解决线程之间的共享变量问题 | 线程之间的数据隔离 |
运用场景:
转账的JDBC的connection问题:JDBC Connection 类是非线程安全的,两个线程不能安全地共享一个 Connection。
当线程A获取到Connection,开启一个事务,正在在执行事务,但是未结束。此时,线程B也获取到Connection,它发送了一些SQL操作,这些SQL操作将会被数据库归入线程A的事务当中被执行,这就造成了张冠李戴。
如果采用线程局部变量的形式,此时Connection为线程A和线程B的局部变量,本质上就是开启了两个Connection,从而可以使线程A和线程B可以相关独立。
service层与dao层的connection是同一个,这样就不会导致出现问题。我们在创建JDBCUtil时,就将ThreadLocal申明出来
在线程并发情况下每个线程都只能操作自己的connection。
ThreadLocal原理
Thread中维护了一个ThreadLocalMap, ThreadLocalMap的Key就是ThreadLocal,线程中的局部变量就是value的值。
具体描述:
- 每个Thread线程内部都有一个Map(ThreadLocalMap)
- Map里面存储ThreadLocal对象(Key)和线程的变量副本(value)
- Thread内部的Map是由ThreadLocal维护的,由ThreadLocal向Map获取和设置线程的变量值。
- 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
下面是JDK的早期设计
JDK8的设计方案两个好处:
- 每个Map存储的Entry数量变少
- 当每个Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存的使用。
ThreadLocal的初始化方法:
返回当前线程对应的ThreadLocal的初始值。
此方法的第一次调用发生在,当线程通过get方法访问ThreadLocal值时;一般情况下,每个线程调用一次这个方法。
若是想ThreadLocal有一个不是null的初始值,我们就需要通过子类继承(@code ThreadLocal)的方式去重写此方法,通过匿名内部类的方式实现。
protected T initiaValue(){
return null
}
基本结构
存储结构--Entry
源码展示
Entry继承WeakReference。也就是key(ThreadLocal)是弱引用,其目的是ThreadLocal对象的生命周期和线程的生命周期解绑。
ThreadLocal为什么要使用弱引用
为了解决内存泄漏。
为什么用弱引用就能解决呢?
什么是内存泄漏?
内存中已动态分配的堆内存由于某种原因程序未释放或者无法释放,造成系统内存的浪费。
如果key使用强引用,会出现内存泄漏吗?
ThreadLocalRef使用完回收,接下来就是ThreadLocal,但是由于是强引用,所以无法回收。
完全无法避免内存泄漏的。
如果key使用弱引用,会出现内存泄漏吗?
ThreadLocalRef使用完回收;但是ThreadLocal使用的是弱连接,所以也可以没有连接到ThreadLocal的线程。我们将ThreadLocal回收即可。由于我们的Map是由Entry数据结构实现的,所以若是不能够删除,还是会造成内存泄漏的。
要避免内存泄漏的两种方法:
使用完ThreadLocal,调用其remove()方法删除对应的Entry
使用完ThreadLocal,当前线程Thread也随之运行结束。