深入理解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.创建子线程的时候传入父线程的变量,并将其赋值到子线程

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jsxllht

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值