深入理解ThreadLocal
深入理解ThreadLocal
一、ThreadLocal
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的
因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
public class SequenceNumber {
//①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>(){
public Integer initialValue(){
return 0;
}
};
//②获取下一个序列值
public int getNextNum(){
seqNum.set(seqNum.get()+1);
return seqNum.get();
}
public static void main(String[ ] args)
{
SequenceNumber sn = new SequenceNumber();
//③ 3个线程共享sn,各自产生序列号
TestClient t1 = new TestClient(sn);
TestClient t2 = new TestClient(sn);
TestClient t3 = new TestClient(sn);
t1.start();
t2.start();
t3.start();
}
private static class TestClient extends Thread
{
private SequenceNumber sn;
public TestClient(SequenceNumber sn) {
this.sn = sn;
}
public void run() {
//④每个线程打出3个序列值
for (int i = 0; i < 3; i++) {
System.out.println("thread["+Thread.currentThread().getName()+ "] sn["+sn.getNextNum()+"]");
}
}
}
}
二、ThreadLocal与Synchronized的区别
ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。
但是ThreadLocal与synchronized有本质的区别:
1、Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
2、Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
一句话理解ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。
三、ThreadLocal核心方法
ThreadLocal中set方法
ThreadLocal.ThreadLocalMap threadLocals = null;
public void set(T value) {
//返回对当前执行的线程对象的引用。
Thread t = Thread.currentThread();
//获取与ThreadLocal关联的映射,InheritableThreadLocal<T> extends ThreadLocal<T> 重写了getMap()方法
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//InheritableThreadLocal中重写了createMap->初始化当前线程的ThreadLocalMap->ThreadLocal.ThreadLocalMap
createMap(t, value);
}
ThreadLocal中get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//返回初始化的值null
return setInitialValue();
}
ThreadLocal中remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
remove方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。为什么要删除,这涉及到内存泄露的问题。
ThreadLocal中initialValue方法
protected T initialValue() {
return null;
}
返回该线程局部变量的初始值,若使用protected限制父类的方法,则该方法仅父类和子类内部(即定义父类和子类的代码中)可以调用,所以这个方法显然是为了子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
Entry
ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用,因此当value=null时意味着该键不再被引用可以被垃圾回收 。在Entry内部使用ThreadLocal作为key,
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。
ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例字,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。
四、ThreadLocal与Thread,ThreadLocalMap之间的关系
(1)每个Thread线程内部都有一个Map (ThreadLocalMap)
( 2 ) Map里面存储ThreadLocal对象(key )和线程的变量副本( value )
( 3 ) Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变星值。
( 4 ) 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
五、ThreadLocal 常见使用场景
1、每个线程需要有自己单独的实例
2、实例需要在多个方法中共享,但不希望被多线程共享
对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。
对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。
场景一 ThreadLocal来存储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;
}
场景二 解决线程安全的问题
比如Java7中的SimpleDateFormat不是线程安全的,可以用ThreadLocal来解决这个问题:
public class DateUtil {
private static final String dateFormatStr = "yyyy-MM-dd HH:mm:ss";
private static ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(dateFormatStr);
}
};
public static String formatDate(Date date) {
return dateFormat.get().format(date);
}
}
这里的DateUtil.formatDate()就是线程安全的了。(Java8里的 java.time.format.DateTimeFormatter是线程安全的,Joda time里的DateTimeFormat也是线程安全的)。
场景三、使用切面打印日志开始到结束使用ThreadLocal解决
@Component
@Slf4j
@Aspect
public class Aspect1 {
ThreadLocal<Long> startTime = new ThreadLocal<>();
@Pointcut("execution(* com.*(..))")
public void webLog(){}
@Before(value = "webLog()")
public void before(JoinPoint joinPoint) {
//输出连接点的信息
startTime.set(System.currentTimeMillis());
//日志操作
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
log.info("****************HeaderStart***********************");
while (headerNames.hasMoreElements()){
String headerName = headerNames.nextElement();
log.info("*****<{}: {}>",headerName,request.getHeader(headerName));
}
log.info("****************HeaderEnd***********************");
//------------其他处理
}
@AfterReturning(returning = "ret", value = "webLog()")
public void afterThrowing(String ret) {
log.info("RESPONSE: {}",ret);
log.info("SPEND TIME: {}",System.currentTimeMillis()-startTime.get());
}
}
场景四、ThreadLocal在Spring事务管理中的应用
Spring使用ThreadLocal解决线程安全问题
在一般情况下,只有无状态的Bean从才能在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。(PS:Spring Bean实例的作用范围,一般由scope进行指定,scope配置项有5个属性,用于描述不同的作用域:1.singleton:使用该属性定义Bean时,IOC容器仅创建一个Bean实例,IOC容器每次返回的是同一个Bean实例。2.prototype:使用该属性定义Bean时,IOC容器可以创建多个Bean实例,每次返回的都是一个新的实例。)
绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean中非线程安全的”状态性对象“采用ThreadLocal进行封装。因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。一般情况下,从接收请求到返回响应所经过的所有程序调用都属于同一个线程。
下面的示例能够体现Spring对有状态Bean的改造思路:
public class TopicDao{
private Connection conn;//一个非线程安全的变量
public void addTopic(){
Statement stat=conn.createStatement();
}
}
由于这个conn是一个非线程安全的成员变量,因此addTopic()方法是非线程安全的,必须在使用时创建一个新的TopicDao实例。下面,使用ThreadLocal对conn这个非线程安全的状态进行改造:
import java.sql.Connection;
import java.sql.Statement;
public class TopicDao{
private static ThreadLocal<Connection> connThreadLocal=new ThreadLocal<Connection>();//使用ThreadLocal保存Connection变量
public static Connection getConnection(){
if(connThreadLocal.get()==null){//如果connThreadLocal没有本线程对应的Connection创建一个新的Connection
Connection conn=ConnectionManager.getConnection();
connThreadLocal.set(conn);
return conn;
}
else{
return connThreadLocal.get();//直接返回线程本地变量
}
}
public void addTopic(){
Statement stat=getConnection().createStatement();
}
}
不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否为null,如果是null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中,如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用自己独立的Connection,而不会使用其他线程的Connection,因此,这个TopicDao就可以做到singleton共享了。
六、ThreadLocal其他几个注意的点
ThreadLocal 内存泄露的原因
Entry将ThreadLocal作为Key,值作为value保存,它继承自WeakReference,注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个「弱引用」。可以看图1.
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
主要两个原因
1 . 没有手动删除这个 Entry
2 . CurrentThread 当前线程依然运行
第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法删除对应的 Entry ,就能避免内存泄漏。
第二点稍微复杂一点,由于ThreadLocalMap 是 Thread 的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期跟 Thread 一样长。如果threadlocal变量被回收,那么当前线程的threadlocal 变量副本指向的就是key=null, 也即entry(null,value),那这个entry对应的value永远无法访问到。实际上ThreadLocal场景都是采用线程池,而线程池中的线程都是复用的,这样就可能导致非常多的entry(null,value)出现,从而导致内存泄露。
综上, ThreadLocal 内存泄漏的根源是:
由于ThreadLocalMap 的生命周期跟 Thread 一样长,对于重复利用的线程来说,如果没有手动删除(remove()方法)对应 key 就会导致entry(null,value)的对象越来越多,从而导致内存泄漏.
key 如果是强引用
为什么ThreadLocalMap的key要设计成弱引用呢?其实很简单,如果key设计成强引用且没有手动remove(),那么key会和value一样伴随线程的整个生命周期。
假设在业务代码中使用完ThreadLocal, ThreadLocal ref被回收了,但是因为threadLocalMap的Entry强引用了threadLocal(key就是threadLocal), 造成ThreadLocal无法被回收。在没有手动删除Entry以及CurrentThread(当前线程)依然运行的前提下, 始终有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry, Entry就不会被回收( Entry中包括了ThreadLocal实例和value), 导致Entry内存泄漏也就是说: ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。
为什么 key 要用弱引用
事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的.这就意味着使用threadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收.对应value在下一次 ThreadLocaIMap 调用 set/get/remove 中的任一方法的时候会被清除,从而避免内存泄漏.
如何正确的使用ThreadLocal
1、将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露
2、每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
ThreadLocal高级面试真题
一.ThreadLocal 是什么?
ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,适用于各个线程不共享变量值的操作。
二.为什么 ThreadLocalMap 的 key 是弱引用?
1.key使用强引用:这样会导致一个问题,引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,则会导致内存泄漏。
2.key使用弱引用:这样的话,引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候会被清除。
总结:比较以上两种情况,我们可以发现:由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障,弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候被清除,算是最优的解决方案。
三.ThreadLocal类之如何让子类访问父线程的值?
1.InheritableThreadLocal类
继承自ThreadLocal,提供了一个特性,让子线程可以访问父线程中设置的本地变量。InheritableThreadLocal重写了creatMap方法,所以在这个类中inheritableThreadLocals代替了threadLocals,所以get和set的都是这个map
2.创建子线程的时候传入父线程的变量,并将其赋值到子线程
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)