你真的了解 ThreadLocal吗?
1、ThreadLocal是什么?
在多线程编程中,通常解决线程安全的问题我们会利用synchronzed或lock
控制线程对临界区资源的同步顺序从而解决线程安全的问题。
但是这种加锁的方式会让未获取到锁的线程进行阻塞等待,很显然这种方式的时间效率并不是很好!
线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作!
那么,如果每个线程都使用自己的 “共享资源“ 呢?各自使用各自的,又互相不影响到彼此,即让多个线程间达到隔离的状态,这样就不会出现线程安全的问题。
事实上,这就是一种“空间换时间”的方案,每个线程都拥有自己的“共享资源”无疑内存会大很多,但是由于不需要同步也就减少了线程可能存在的阻塞等待情况从而提高的执行效率!
虽然ThreadLocal并不在java.util.concurrent包中,而在java.lang包中,但我更倾向于把它当作是一种并发容器进行归类。
重点来了!
顾名思义,ThreadLocal,即线程本地变量。 如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝、达到人手一份的效果,多个线程操作这个变量的时候,实际是操作自己本地内存里的变量,从而起到线程隔离的作用,避免了线程安全问题、避免了共享资源带来的竞争!
//创建一个ThreadLocal变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();
2、ThreadLocal的底层原理
2.1 ThreadLocal到底是什么类型?
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap类型的成员变量:
ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值:
每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离!
2.2 ThreadLocal的内存结构图
由结构图是可以看出:
Thread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量。
ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
3、知道ThreadLocal的内存泄漏问题吗
3.1 Java中的4种引用类型
- 强引用:我们常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
- 软引用:使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
- 弱引用:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
- 虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
3.2 再来看内存泄漏问题
先看看一下的TreadLocal的引用示意图:
ThreadLocalMap中使用的 key 为 ThreadLocal 的 弱引用:
弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存!
3.3 如何避免内存泄漏问题
由上可知,弱引用比较容易被回收!
因此,如果ThreadLocal.ThreadLocalMap中的Key被垃圾回收器回收了,
但是因为ThreadLocal.ThreadLocalMap生命周期和Thread是一样的,如果它这时候不被回收,就会出现:
ThreadLocalMap的key没了,value还在!这就「造成了内存泄漏问题」!
那么,如何 「解决内存泄漏问题」 ?
答案:使用完ThreadLocal后,及时调用remove()方法释放内存空间!
4、ThreadLocal的典型应用场景
4.1 Session的管理
Hibernate中通过ThreadLocal管理Session就是一个典型的案例,不同的请求线程(用户)拥有自己的session,若将session共享出去被多线程访问,必然会带来线程安全问题。
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;
}
4.2 在线程内部创建ThreadLocal
还有一种用法是在线程类内部创建ThreadLocal,基本步骤:
- 在多线程的类ThreadLocalTest中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx
- 在ThreadLocalTest类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型
- 在ThreadLocalTest类的run()方法,通过调用getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象
public class ThreadLocalTest implements Runnable{
ThreadLocal<Student> studentThreadLocal = new ThreadLocal<Student>();
@Override
public void run() {
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName + " is running...");
Random random = new Random();
int age = random.nextInt(100);
System.out.println(currentThreadName + " is set age: " + age);
Student Student = getStudent(); //通过这个方法,为每个线程都独立的new一个Studentt对象,每个线程的的Student对象都可以设置不同的值
Student.setAge(age);
System.out.println(currentThreadName + " is first get age: " + Student.getAge());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( currentThreadName + " is second get age: " + Student.getAge());
}
private Student getStudent() {
Student Student = studentThreadLocal.get();
if (null == Student) {
Student = new Student();
studentThreadLocal.set(Student);
}
return Student;
}
public static void main(String[] args) {
ThreadLocalTest t = new ThreadLocalTest();
Thread t1 = new Thread(t,"Thread A");
Thread t2 = new Thread(t,"Thread B");
t1.start();
t2.start();
}
}
class Student{
int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
4.3 时间格式化工具类
看看阿里巴巴 Java 开发手册中推荐的 ThreadLocal 的用法:
import java.text.DateFormat;
import java.text.SimpleDateFormat;
public class DateUtils {
public static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
}
然后我们再要用到 DateFormat 对象的地方,这样调用:
DateUtils.df.get().format(new Date());
5、总结
5.1 为什么要有ThreadLocal?
在多线程编程中,通常解决线程安全的问题我们会利用synchronzed或lock
控制线程对临界区资源的同步顺序从而解决线程安全的问题。但是这种加锁的方式会让未获取到锁的线程进行阻塞等待,很显然这种方式的时间效率并不是很好!
ThreadLocal,即线程本地变量。 如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝、达到人手一份的效果,多个线程操作这个变量的时候,实际是操作自己本地内存里的变量,从而起到线程隔离的作用,避免了线程安全问题、避免了共享资源带来的竞争!
5.2 ThreadLocal到底是什么类型?
Thread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量。
ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
5.3 内存泄漏问题
弱引用比较容易被回收!
因此,如果Threadlocal.ThreadLocalMap中的Key被垃圾回收器回收了,
但是因为Threadlocal.ThreadLocalMap生命周期和Thread是一样的,如果它这时候不被回收,就会出现:
ThreadLocalMap的key没了,value还在!这就「造成了内存泄漏问题」!
那么,如何 「解决内存泄漏问题」?
答案:使用完ThreadLocal后,及时调用remove()方法释放内存空间!
5.4 典型应用场景
1、Hibernate中通过ThreadLocal管理Session!
2、在线程内部创建ThreadLocal对象用来保存线程间需要隔离处理的对象!
3、阿里巴巴Java开发手册中推荐的基于ThreadLocal实现的时间格式化工具类!