ThreadLocal使用与原理

这篇具有很好参考价值的文章主要介绍了ThreadLocal使用与原理。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、ThreadLocal简介


ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景

下图可以增强理解:

ThreadLocal使用与原理

二、ThreadLocal原理

ThreadLocal变量只在单个线程内可见,那它是如何做到的呢?我们先从最基本的get()方法说起:

public T get() {
    //获得当前线程
    Thread t = Thread.currentThread();
    //每个线程 都有一个自己的ThreadLocalMap,
    //ThreadLocalMap里就保存着所有的ThreadLocal变量
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //ThreadLocalMap的key就是当前ThreadLocal对象实例,
        //多个ThreadLocal变量都是放在这个map中的
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            //从map里取出来的值就是我们需要的这个ThreadLocal变量
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map没有初始化,那么在这里初始化一下
    return setInitialValue();
}

可以看到,所谓的ThreadLocal变量就是保存在每个线程的map中的。这个map就是Thread对象中的threadLocals字段。如下:

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal.ThreadLocalMap是一个比较特殊的Map,它的每个Entry的key都是一个弱引用:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    //key就是一个弱引用
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

这样设计的好处是,如果这个变量不再被其他对象使用时,可以自动回收这个ThreadLocal对象,避免可能的内存泄露。

三、ThreadLocal中的内存泄漏问题

虽然ThreadLocalMap中的key是弱引用,当不存在外部强引用的时候,就会自动被回收,但是Entry中的value依然是强引用。这个value的引用链条如下:

ThreadLocal使用与原理

ThreadLocalMap在内存中的关系如下图:

ThreadLocal使用与原理

 可以看到,只有当Thread被回收时,这个value才有被回收的机会,否则,只要线程不退出,value总是会存在一个强引用。但是,要求每个Thread都会退出,是一个极其苛刻的要求,对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话,就会造成value对象出现泄漏的可能。处理的方法是,在ThreadLocalMap进行set(),get(),remove()的时候,都会进行清理:

以getEntry()为例:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        //如果找到key,直接返回
        return e;
    else
        //如果找不到,就会尝试清理,如果你总是访问存在的key,那么这个清理永远不会进来
        return getEntryAfterMiss(key, i, e);
}

 下面是getEntryAfterMiss()的实现:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        // 整个e是entry ,也就是一个弱引用
        ThreadLocal<?> k = e.get();
        //如果找到了,就返回
        if (k == key)
            return e;
        if (k == null)
            //如果key为null,说明弱引用已经被回收了
            //那么就要在这里回收里面的value了
            expungeStaleEntry(i);
        else
            //如果key不是要找的那个,那说明有hash冲突,这里是处理冲突,找下一个entry
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

真正用来回收value的是expungeStaleEntry()方法,在remove()和set()方法中,都会直接或者间接调用到这个方法进行value的清理:

从这里可以看到,ThreadLocal为了避免内存泄露,也算是花了一番大心思。不仅使用了弱引用维护key,还会在每个操作上检查key是否被回收,进而再回收value。

但是从中也可以看到,ThreadLocal并不能100%保证不发生内存泄漏。

比如,很不幸的,你的get()方法总是访问固定几个一直存在的ThreadLocal,那么清理动作就不会执行,如果你没有机会调用set()和remove(),那么这个内存泄漏依然会发生。

因此,一个良好的习惯依然是:当你不需要这个ThreadLocal变量时,主动调用remove(),这样对整个系统是有好处的

四、ThreadLocal的使用

(一)简单使用

假设循环创建1000个线程,每个线程都用当前次循环的i值去调用say方法,方法中把对应参数值赋给一个静态变量num,然后还让线程sleep1毫秒模拟执行复杂业务逻辑,然后再输出num,同时往threadlocal中放入一开始的num。

public class School {

    static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    static int num ;

    static void say(int time) throws InterruptedException {
        num = time;
        threadLocal.set(num+"");
        Thread.sleep(1);
        System.out.println("从ThreadLocal拿出来的:" + threadLocal.get());
        System.out.println("从当前方法的静态变量中的:"+num);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            // 线程创建
            int finalI = i;
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        say(finalI);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }
}

查看结果:

ThreadLocal使用与原理

不难看出从当前方法的静态变量中拿出来打印的很多重复值,发生了典型的值覆盖的线程安全问题,而从ThreadLocal中取出来的就是保证了没有重复值,这就是其中一个应用场景,典型的这个线程不安全的场景就有大名鼎鼎的日期格式化工具simpledateformat,多线程传进去的time会被覆盖。

(二)复杂使用

我们先看JdbcTemplate类数据访问类

public Object execute(ConnectionCallback action)
    throws DataAccessException
  {
    Assert.notNull(action, "Callback object must not be null");
    Connection con = DataSourceUtils.getConnection(getDataSource());
    try {
      Connection conToUse = con;
      if (this.nativeJdbcExtractor != null)
      {
        conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
      }
      else
      {
        conToUse = createConnectionProxy(con);
      }
      localObject1 = action.doInConnection(conToUse);
    }
    catch (SQLException ex)
    {
      Object localObject1;
      DataSourceUtils.releaseConnection(con, getDataSource());
      con = null;
      throw getExceptionTranslator().translate("ConnectionCallback", getSql(action), ex);
    }
    finally {
      DataSourceUtils.releaseConnection(con, getDataSource());
    }
  }

  由上述源码中Connection con = DataSourceUtils.getConnection(getDataSource());这个可以看出,DataSourceUtils类保证当前线程获得的是同一个Connection对象。下面我们主要分析DataSourceUtils类:

public static Connection getConnection(DataSource dataSource)
    throws CannotGetJdbcConnectionException
  {
    try
    {
      return doGetConnection(dataSource);
    } catch (SQLException ex) {
    }
    throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
  }
  public static Connection doGetConnection(DataSource dataSource)
    throws SQLException
  {
    Assert.notNull(dataSource, "No DataSource specified");
    ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
    if ((conHolder != null) && ((conHolder.hasConnection()) || (conHolder.isSynchronizedWithTransaction()))) {
      conHolder.requested();
      if (!conHolder.hasConnection()) {
        logger.debug("Fetching resumed JDBC Connection from DataSource");
        conHolder.setConnection(dataSource.getConnection());
      }
      return conHolder.getConnection();
    }
    logger.debug("Fetching JDBC Connection from DataSource");
    Connection con = dataSource.getConnection();
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
      logger.debug("Registering transaction synchronization for JDBC Connection");
      ConnectionHolder holderToUse = conHolder;
      if (holderToUse == null) {
        holderToUse = new ConnectionHolder(con);
      }
      else {
        holderToUse.setConnection(con);
      }
      holderToUse.requested();
      TransactionSynchronizationManager.registerSynchronization(new ConnectionSynchronization(holderToUse, dataSource));
      holderToUse.setSynchronizedWithTransaction(true);
      if (holderToUse != conHolder) {
        TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
      }
    }
    return con;
  }

  由以上源码可以知道,数据库连接从TransactionSynchronizationManager中获得,如果已经存在则获得,否则重新从DataSource创建一个连接,并把这个连接封装为ConnectionHolder,然后注册绑定到TransactionSynchronizationManager中,并返回Connection对象。同时,可以看出DataSource和ConnectionHolder的存储管理在TransactionSynchronizationManager中,继续分析TransactionSynchronizationManager中的关键代码:

private static final ThreadLocal resources = new NamedThreadLocal("Transactional resources");
  public static Object getResource(Object key)
  {
    Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
    Object value = doGetResource(actualKey);
    if ((value != null) && (logger.isTraceEnabled())) {
      logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
    }
    return value;
  }
  private static Object doGetResource(Object actualKey)
  {
    Map map = (Map)resources.get();
    if (map == null) {
      return null;
    }
    Object value = map.get(actualKey);
    if (((value instanceof ResourceHolder)) && (((ResourceHolder)value).isVoid())) {
      map.remove(actualKey);
      value = null;
    }
    return value;
  }
  public static void bindResource(Object key, Object value)
    throws IllegalStateException
  {
    Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
    Assert.notNull(value, "Value must not be null");
    Map map = (Map)resources.get();
    if (map == null) {
      map = new HashMap();
      resources.set(map);
    }
    if (map.put(actualKey, value) != null) {
      throw new IllegalStateException("Already value [" + map.get(actualKey) + "] for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
    }
    if (logger.isTraceEnabled())
      logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" + Thread.currentThread().getName() + "]");
  }

分析源码可以得出,
(1)TransactionSynchronizationManager内部用ThreadLocal对象存储资源,ThreadLocal存储的为DataSource生成的actualKey为key值和ConnectionHolder作为value值封装成的Map。
(2)结合DataSourceUtils的doGetConnection函数和TransactionSynchronizationManager的bindResource函数可知:在某个线程第一次调用时候,封装Map资源为:key值为DataSource生成actualKey【Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);】value值为DataSource获得的Connection对象封装后的ConnectionHolder。等这个线程下一次再次访问中就能保证使用的是第一次创建的ConnectionHolder中的Connection对象。

  当事务结束后,调用【DataSourceUtils.releaseConnection(con, getDataSource());】将ConnectionHolder从TransactionSynchronizationManager中解除。当谁都不用,这个连接被close。 
 

public static void releaseConnection(Connection con, DataSource dataSource)
  {
    try
    {
      doReleaseConnection(con, dataSource);
    }
    catch (SQLException ex) {
      logger.debug("Could not close JDBC Connection", ex);
    }
    catch (Throwable ex) {
      logger.debug("Unexpected exception on closing JDBC Connection", ex);
    }
  }
  public static void doReleaseConnection(Connection con, DataSource dataSource)
    throws SQLException
  {
    if (con == null) {
      return;
    }
    if (dataSource != null) {
      ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
      if ((conHolder != null) && (connectionEquals(conHolder, con)))
      {
        conHolder.released();
        return;
      }
    }
    if ((!(dataSource instanceof SmartDataSource)) || (((SmartDataSource)dataSource).shouldClose(con))) {
      logger.debug("Returning JDBC Connection to DataSource");
      con.close();
    }
  }

参考文章:

ThreadLocal使用与原理 - 知乎

史上最全ThreadLocal 详解(一)_倔强的不服的博客-CSDN博客

ThreadLocal从简单使用及源码_threadlocal代码使用_凉拌海蜇丝的博客-CSDN博客

Spring事务之如何保证同一个Connection对象_HaiwiSong的博客-CSDN博客文章来源地址https://www.toymoban.com/news/detail-512589.html

到了这里,关于ThreadLocal使用与原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 深入剖析ThreadLocal使用场景、实现原理、设计思想

    ThreadLocal可以用来存储线程的本地数据,做到线程数据的隔离 ThreadLocal的使用不当可能会导致内存泄漏,排查内存泄漏的问题,不仅需要熟悉JVM、利用好各种分析工具还耗费人工 如果能明白其原理并正确使用,就不会导致各种意外发生 本文将从使用场景、实现原理、内存泄漏

    2024年02月08日
    浏览(64)
  • ThreadLocal的使用介绍和底层原理解析和开源框架的使用实例

    ThreadLocal是一个线程内部的数据存储类,它可以为每个线程提供独立的变量副本,不同线程间的变量无法相互访问和修改。这避免了每个线程都要维护一套独立变量的麻烦,并且也减少了线程之间不必要的数据争用。ThreadLocal适用于这样的场景:每个线程需要有自己单独的实例,而不

    2024年02月02日
    浏览(46)
  • ThreadLocal 本地线程变量详解

    ThreadLocal 意为本地线程变量,即该变量只属于当前线程,对其他线程隔离 我们知道,一个普通变量如果被多线程访问会存在存在线程安全问题,这时我们可以使用 Synchronize 来保证该变量某一时刻只能有一个线程访问,从而解决并发安全问题 但如果这个变量并不需要被共享,

    2024年02月05日
    浏览(47)
  • 线程安全—ThreadLocal

    定义: ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为每个线程都创建了一个副本,存的是自己,那么每个线程可以访问自己内部的副本变量。 ThreadLocal是用空间换取时间,synchronized是用时间换空间。

    2024年02月11日
    浏览(36)
  • ThreadLocal:线程中的全局变量

    最近接了一个新需求,业务场景上需要在原有基础上新增2个字段,接口新增参数意味着很多类和方法的逻辑都需要改变,需要先判断是否属于该业务场景,再做对应的逻辑。原本的打算是在入口处新增变量,在操作数据的时候进行逻辑判断将变量进行存储或查询。 如果全链

    2024年02月10日
    浏览(42)
  • 【Java】线程数据共享和安全 -ThreadLocal

     🎄欢迎来到@边境矢梦°的csdn博文🎄  🎄本文主要梳理线程数据共享和安全 -ThreadLocal🎄 🌈我是边境矢梦°,一个正在为秋招和算法竞赛做准备的学生🌈 🎆喜欢的朋友可以关注一下 🫰🫰🫰 ,下次更新不迷路🎆 Ps: 月亮越亮说明知识点越重要 (重要性或者难度越大)🌑🌒

    2024年02月09日
    浏览(43)
  • ThreadLocal加切面实现线程级别的方法缓存

    当一个请求线程多次请求A方法时,只会触发一次A方法的实际调用,会将方法结果缓存起来,避免多次调用。 1. 需要一个注解ThreadLocalCache,在需要缓存的方法上加上该注解 2. 需要一个切面,借助ThreadLocal,将结果缓存起来,利用环绕通知来实现方法拦截从缓存中返回方法执行结果 3.1、

    2024年04月10日
    浏览(40)
  • 【Java 并发编程】Java 线程本地变量 ThreadLocal 详解

    先一起看一下 ThreadLocal 类的官方解释: 用大白话翻译过来,大体的意思是: ThreadLoal 提供给了 线程局部变量 。同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意: 因为每个 Thread 内有自己的实例副本,且 该副本只能由当前 Thread 使用 。

    2024年02月04日
    浏览(71)
  • ThreadLocal的应用及原理

    JDK 对 ThreadLocal 的描述为: 此类提供线程局部变量。这些变量与普通变量的不同之处在于,每个访问一个变量的线程(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段,这些字段希望将状态与线程(例如,用户ID或事务ID)相

    2024年02月06日
    浏览(57)
  • ThreadLocal 的应用及原理

    JDK 对 ThreadLocal 的描述为: 此类提供线程局部变量。这些变量与普通变量的不同之处在于,每个访问一个变量的线程(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段,这些字段希望将状态与线程(例如,用户ID或事务ID)相

    2024年02月06日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包