前言

ThreadLocal 的经典使用场景是数据库连接、 session 管理、多线程等……

比如在Spring中,发挥着巨大的作用,在管理Request作用域中的Bean、事务管理、任务调度、AOP等模块都不同程度使用了ThreadLocal 。
Spring中绝大部分Bean,都可以声明成Singleton作用域,采用ThreadLocal进行封装,因此有状态的Bean,就能够以singleton的方式,在多线程中正常工作。

知道Threadlocal怎么用,但是不知道为什么要这样用?底层原理是什么?Threadlocal发生hashmap的hash冲突,怎么办?

threadlocal是什么?

ThreadLocal提供线程局部变量。

//get()方法是用来获取ThreadLocal在当前线程中保存的变量副本
public T get() { }
//set()用来设置当前线程中变量的副本
public void set(T value) { }
//remove()用来移除当前线程中变量的副本
public void remove() { }
//initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法
protected T initialValue(){ }

这些变量与普通的变量不同之处在于,每个访问这种变量的线程(通过它的get或set方法)都有自己的、独立初始化的变量副本
ThreadLocal实例,通常是希望将状态关联到一个线程的类的私有静态字段(比如,user ID 或者 Transaction ID 等等)。

总而言之:

  1. ThreadLocal是一种变量类型,我们称之为“线程局部变量”。
  2. 每个线程访问这种变量的时候,都会创建该变量的副本,这个变量副本为线程私有。
  3. ThreadLocal类型的变量,一般用private static加以修饰。

例如,下面的例子中这个类为每个线程生成唯一标识。一个线程的id是它第一次调用ThreadId.get()方法指定的。

package com.azdebugit.threadlocal;public class ThreadLocalExsample {private static   ThreadLocal<Long> longLocal = new ThreadLocal<>();public void set() {longLocal.set(Thread.currentThread().getId());}public long getLong() {return longLocal.get();}public static void main(String[] args) {ThreadLocalExsample test = new ThreadLocalExsample();//注意:没有set之前,直接get,报null异常了test.set();System.out.println("-------threadLocal value-------" + test.getLong());longLocal.remove();}
}

ThreadLocal的应用场景

注意:使用ThreadLocal时,先进行get之前,必须先set,否则会报空指针异常

数据库连接

@Component
public class ConnectionHolderUtil {private static DataSource dataSource;private static final Logger  log =  LoggerFactory.getLogger(ConnectionHolderUtil.class);@Autowiredpublic void setDataSource(DataSource dataSource) {ConnectionHolderUtil.dataSource = dataSource;}private static ThreadLocal<ConnectionHolder> connectionHolderThreadLocal = new ThreadLocal<>();/** * 获取数据库连接 * @return Connection */public static ConnectionHolder getConnectionHolder(boolean isNew){ConnectionHolder connectionHolder = connectionHolderThreadLocal.get();//如果有连接,并不需要生成新的直接返回if(connectionHolder != null && !isNew){return connectionHolder;}try {//获取新连接Connection connection = dataSource.getConnection();//关闭自动提交connection.setAutoCommit(false);connectionHolder = new ConnectionHolder(connection);connectionHolderThreadLocal.set(connectionHolder);//绑定连接TransactionSynchronizationManager.bindResource(dataSource,connectionHolder);return connectionHolder;} catch (SQLException e) {log.error("数据库连接获取失败",e);return null;}}/** * 提交事务 */public static void commit(){ConnectionHolder connectionHolder = connectionHolderThreadLocal.get();if(connectionHolder == null){return;}try {connectionHolder.getConnection().commit();} catch (SQLException e) {log.error("提交失败",e);}}/** * 事务回滚 */public static void rollback(){ConnectionHolder connectionHolder = connectionHolderThreadLocal.get();if(connectionHolder == null){return;}try {connectionHolder.getConnection().rollback();} catch (SQLException e) {log.error("回滚失败",e);}}/** * 关闭连接 */public static void close(){ConnectionHolder connectionHolder = connectionHolderThreadLocal.get();if(connectionHolder == null){return;}Connection connection = connectionHolder.getConnection();try {connection.close();} catch (SQLException e) {log.error("数据库连接关闭失败",e);}}/** * 恢复挂起的事务 */public static void resume(Object susPend){TransactionSynchronizationManager.unbindResource(dataSource);TransactionSynchronizationManager.bindResource(dataSource,susPend);connectionHolderThreadLocal.set((ConnectionHolder) susPend);}/** * 挂起当前事务 */public static Object hangTrasaction(){return TransactionSynchronizationManager.unbindResource(dataSource);}/** * 判断当前连接是否已经关闭 * @return */public static boolean isClose(){if(connectionHolderThreadLocal.get() == null){return true;}try {return connectionHolderThreadLocal.get().getConnection().isClosed();} catch (SQLException e) {log.error("获取连接状态失败");}return true;}
}

Session管理

@SuppressWarnings("unchecked")
public class UserSession {  private static final ThreadLocal SESSION_MAP = new ThreadLocal();  protected UserSession() {  }  public static Object get(String attribute) {  Map map = (Map) SESSION_MAP.get(); return map.get(attribute);  }  public static <T> T get(String attribute, Class<T> clazz) {  return (T) get(attribute);  }  public static void set(String attribute, Object value) {  Map map = (Map) SESSION_MAP.get();  if (map == null) {  map = new HashMap();  SESSION_MAP.set(map);  }  map.put(attribute, value);  }
}  

多线程

package com.azdebugit.threadlocal;import java.util.concurrent.atomic.AtomicInteger;public class ThreadLocalExsampl {/*** 创建了一个MyRunnable实例,并将该实例作为参数传递给两个线程。两个线程分别执行run()方法,* 并且都在ThreadLocal实例上保存了不同的值。如果它们访问的不是ThreadLocal对象并且调用的set()方法被同步了,* 则第二个线程会覆盖掉第一个线程设置的值。但是,由于它们访问的是一个ThreadLocal对象,* 因此这两个线程都无法看到对方保存的值。也就是说,它们存取的是两个不同的值。*/public static class MyRunnable implements Runnable {/*** 例化了一个ThreadLocal对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。* 虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却只能访问到自己通过调用ThreadLocal的* set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值,* 他们仍然无法访问到对方的值。*/private static ThreadLocal threadLocal = new ThreadLocal();@Overridepublic void run() {//一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值AtomicInteger atomicInteger = new AtomicInteger();int threadLo = (int) (Math.random() * 100D);System.out.println("-------"+atomicInteger.incrementAndGet()+"-------" + threadLo);threadLocal.set(threadLo);try {Thread.sleep(2000);} catch (InterruptedException e) {}//可以通过下面方法读取保存在ThreadLocal变量中的值System.out.println("-------"+atomicInteger.incrementAndGet()+"-------"+threadLocal.get());threadLocal.remove();}}public static void main(String[] args) {MyRunnable sharedRunnableInstance = new MyRunnable();for (int i = 0; i < 5; i++) {Thread thread1 = new Thread(sharedRunnableInstance);Thread thread2 = new Thread(sharedRunnableInstance);thread1.start();thread2.start();}}
}

hashmap的hash冲突

hash冲突--源码分析

HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置。

当程序执行 map.put(String,Obect)方法 时,系统将调用String的 hashCode() 方法,得到其 hashCode 值(每个 Java 对象都有 hashCode() 方法,都可通过该方法获得它的 hashCode 值)。

得到这个对象的 hashCode 值之后,系统会根据该 hashCode 值来决定该元素的存储位置。源码如下:

public V put(K key, V value) {if (key == null)return putForNullKey(value);int hash = hash(key.hashCode());int i = indexFor(hash, table.length);for (Entry<K,V> e = table[i]; e != null; e = e.next) {Object k;//判断当前确定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那么新值覆盖原来的旧值,并返回旧值。//如果存在相同的hashcode,那么他们确定的索引位置就相同,这时判断他们的key是否相同,如果不相同,这时就是产生了hash冲突。//Hash冲突后,那么HashMap的单个bucket里存储的不是一个 Entry,而是一个 Entry 链。//系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),//那系统必须循环到最后才能找到该元素。if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;e.value = value;return oldValue;}}modCount++;addEntry(hash, key, value, i);return null;
}

当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算,并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。

链式地址法--解决散列值的冲突

Hashmap里面的bucket,出现了单链表的形式,散列表要解决的一个问题,就是散列值的冲突问题,通常是两种方法:链表地址法和开放地址法。

  • 链表法,就是将相同hash值的对象,组织成一个链表,放在hash值对应的槽位;
  • 开放地址法,是通过一个探测算法,当某个槽位已经被占据的情况下,继续查找下一个可以使用的 槽位。

java.util.HashMap采用的链表法的方式,链表是单向链表。形成单链表的核心代码如下:

void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];table[bucketIndex] = new Entry<K,V>(hash, key, value, e);if (size++ >= threshold)resize(2 * table.length);
}

上面方法的代码很简单,但其中包含了一个设计:系统总是将新添加的 Entry 对象,放入 table 数组的 bucketIndex 索引处。

  • 如果 bucketIndex 索引处,已经有了一个 Entry 对象,那新添加的 Entry 对象,指向原有的 Entry 对象(产生一个 Entry 链)。
  • 如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链
  1. HashMap里面没有出现hash冲突时,没有形成单链表时,hashmap查找元素很快,get()方法能够直接定位到元素
  2. 但是出现单链表后,单 个bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止。
  • 如果恰好要搜索的 Entry ,位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。

当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:

  • 增大负载因子,可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);
  • 减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。

解决Threadlocal的hashmap的hash冲突

Threadlocal如何2层kv的map

每个线程都各自有一张独立的散列表,以ThreadLocal对象作为散列表的key,set方法中的值作为value(第一次调用get方法时,以initialValue方法的返回值作为value)。

如上图,可以ThreadLocal类用两层HashMap的kv,进行对象存储。
外面的HashMap的Key是ThreadID,Value是内层的ThreadLocalMap的维护的Entry(ThreadLocal<?> k, Object v)数组。
内层的HashMap的Key是当前ThreadLocal对象,Value是当前ThreadLocal的值。

所以在Threadlocal中,一个线程中,可能会拥有多个ThreadLocal成员变量,所以内层ThreadLocalMap是为了保存同一个线程中的不同ThreadLocal变量。

ThreadLocal造成的内存泄露和相应解决办法

ThreadLocalMap中用内部静态类Entry表示了散列表中的每一个条目,下面是它的代码

static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}

可以看出Entry类继承了WeakRefrence类,所以一个条目,就是一个弱引用类型的对象(要搞清楚,持有weakRefrence对象的引用是个强引用),那么这个weakRefrence对象,保存了谁的弱引用呢?

我们看到构造函数中有个supe(k),k是ThreadLocal类型对象,super表示是调用父类(weakRefrence)的构造函数,所以说一个entry对象中,存储了ThreadLocal对象的弱引用和这个ThreadLocal对应的value对象的强引用。

那Entry中为什么保存的是key的弱引用呢?
其实这是为了最大程度上减少内存泄露,副作用是同时减少哈希表中的冲突。

当ThreadLocal对象被回收时,对应entry中的key就自动变成null(entry对象本身不为null)。

线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程,就有可能获取到上个线程遗留下来的value值,造成bug。

ThreadLocal-hash冲突及解决方案--线性探测

ThreadLocal对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。

Entry便是ThreadLocalMap里定义的节点,它继承了WeakReference类,定义了一个类型为Object的value,用于存放塞到ThreadLocal里的值。

在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key,只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了。

static class Entry extends WeakReference<ThreadLocal> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal k, Object v) {super(k);value = v;}
}

Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。

和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式,并非链表的方式,而是采用线性探测的方式(开放地址法)

所谓线性探测,就是根据初始key的hashcode值,确定元素在table数组中的位置,如果发现这个位置上,已经有其他key值的元素被占用,则利用固定的算法,寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

核心:由于ThreadLocalMap使用线性探测法,来解决散列冲突,所以实际上Entry[]数组在程序逻辑上,是作为一个环形存在的。

ThreadLocalMap解决Hash冲突的方式,就是简单的步长加1或减1,寻找下一个相邻的位置。

线性探测法:直接使用数组来存储数据。可以想象成一个停车问题。若当前车位已经有车,则你就继续往前开,直到找到下一个为空的车位

/*** Increment i modulo len.*/
private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);
}
/*** Decrement i modulo len.*/
private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1);
}

实现步骤:

  1. 得到 key
  2. 计算得 hashValue
  3. 若不冲突,则直接填入数组
  4. 若冲突,则使 hashValue++ ,也就是往后找,直到找到第一个 data[hashValue] 为空的情况,则填入。若到了尾部可循环到前面。

显然ThreadLocalMap采用线性探测的方式,解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。

所以这里引出的良好建议是:

每个线程只存一个变量,这样所有的线程,存放到map中的Key,都是相同的ThreadLocal,如果一个线程,要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时,会极大的增加Hash冲突的可能

分析Threadlocal内部实现原理,并解决Threadlocal的ThreadLocalMap的hash冲突与内存泄露相关推荐

  1. ThreadLocal是否会引发内存泄露的分析 good

    这篇文章,主要解决一下疑惑: 1. ThreadLocal.ThreadLocalMap中提到的弱引用,弱引用究竟会不会被回收? 2. 弱引用什么情况下回收? 3. JAVA的ThreadLocal和 ...

  2. ThreadLocalMap线性探测法解决hash冲突

    第一.前言 ThreadLocal使用的是自定义的ThreadLocalMap,接下来我们来探究一下ThreadLocalMap的hash冲突解决方式. 第二.ThreadLocal的set()方法 ...

  3. ThreadLocal应用与原理分析

    ThreadLocal的作用 ThreadLocal类用来提供线程内部的局部变量,并且这些变量依靠线程独立存在.可以在多个线程中互不干扰的进行存储数据和修改数据,通过set,get 和remove方法 ...

  4. java thread 内存泄露_Java ThreadLocal 内存泄露问题分析及解决方法。

    前言 在分析ThreadLocal导致的内存泄露前,需要普及了解一下内存泄露.强引用与弱引用以及GC回收机制,这样才能更好的分析为什么ThreadLocal会导致内存泄露呢?更重要的是知道该如何避免这 ...

  5. ThreadLocal作用、原理以及问题

    ThreadLocal 1.ThreadLocal的作用 在多线程访问共享资源时会采取一定的线程同步方式(如:加锁)来解决带来的并发问题.(如图) 使用ThreadLocal对共享资源的访问也可以解决 ...

  6. threadlocal内存泄露_ThreadLocal原理解析

    谈一谈不常见却又不可少的ThreadLocal 在写ThreadLocal之前,需要先巩固下一点相关知识:Java内存模型及共享变量的可见性. 内存模型中所有变量存储在主内存中,当一个线程中要使用某个 ...

  7. c++ socket线程池原理_一篇文章看懂 ThreadLocal 原理,内存泄露,缺点以及线程池复用的值传递问题...

    编辑:业余草来源:https://www.xttblog.com/?p=4946 一篇文章看懂 ThreadLocal 原理,内存泄露,缺点以及线程池复用的值传递问题. ThreadLocal 相信不 ...

  8. ThreadLocal 内存泄露的实例分析

    前言 之前写了一篇深入分析 ThreadLocal 内存泄漏问题是从理论上分析ThreadLocal的内存泄漏问题,这一篇文章我们来分析一下实际的内存泄漏案例.分析问题的过程比结果更重要,理论结合实际 ...

  9. ThreadLocal使用和原理

    实现机制 1.每个Thread对象内部都维护了一个ThreadLocalMap这样一个ThreadLocal的Map,可以存放若干个ThreadLocal. /* ThreadLocal values ...

最新文章

  1. PAT甲级1154 Vertex Coloring :[C++题解]图论、模拟、结构体存边
  2. 兼容IE各版本的纯CSS二级下拉菜单
  3. java服务器测试_正确的方法来测试服务器是否在Java中运行?
  4. [JSConf EU 2018] 大脑控制 Javascript
  5. php copy 文件夹,php删除与复制文件夹及其文件夹下所有文件的实现代码
  6. 常见问题_自动打螺丝机常见问题及解决办法
  7. 员工30年换150万补偿款!佳能珠海关厂 因给太多遭痛骂:恶意拉高赔偿标准
  8. Transaction marked as rollbackOnly异常处理 Duplicate entry 'xxx' for key
  9. 1633: [Usaco2007 Feb]The Cow Lexicon 牛的词典(DP)
  10. 数值分析常见基本算法及MATLAB代码总结
  11. 台式计算机加固态硬盘,台式电脑加固态硬盘教程_固态硬盘台式机安装方法-win7之家...
  12. MPI大漩涡(单纯的floyd)
  13. dreamweaver中灵活的调整表格的宽高
  14. 离谱的 CSS!从表盘刻度到剪纸艺术
  15. 电销行业通讯难题的解决方案出来了!
  16. 马士兵mysql_MYSQL相关总结(马士兵教育)
  17. 安卓街机模拟器 MAME4droid 源码,只需要自己加入rom 可以发布到安卓市场了。
  18. linux下使用PulseAudio获取扬声器的音量和是否静音(C++)
  19. [全流程案例]壮汉:1. 起大形(Blender)
  20. 校赛预赛 第二天我们依然是第一,但后来的插曲足以让我终身铭记啊~~

热门文章

  1. BBEdit使用教程
  2. 360oauth token是什么意思_Oauth/access token
  3. opencv读取视频及打不开视频的解决方法
  4. poi 操作 PPT,针对 PPTX--图表篇
  5. GitHub下载加速网站
  6. 计算机应用基础0039答案,计算机应用基础-0039(贵州电大-课程号:5205004)参考资料.docx...
  7. C++排序——奖学金
  8. 程序员如何选择技术方向
  9. Docker 部署在线文件转换服务--Libre Office Online
  10. python科学数据库(一)