Java读源码之ThreadLocal
前言
JDK版本: 1.8
之前在看Thread源码时候看到这么一个属性
ThreadLocal.ThreadLocalMap threadLocals = null;
作用
ThreadLocal实现的是每个线程都有一个本地的副本,相当于局部变量,这样就可以少一些参数传递,是以空间换时间的一周策略,其实ThreadLocal就是内部自己实现了一个map数据结构。
存在的问题
ThreadLocal确实很重要,但想到看源码还是有个小故事的,之前去美团点评面试,问我如何保存用户登录token,可以避免层层传递token?
心想这好像是在说ThreadLocal,然后开始胡说放在redis里或者搞个ThreadLocal,给自己挖坑了
面试官继续问,ThreadLocal使用时候主要存在什么问题么?
完蛋,确实只了解过,没怎么用过,凉凉,回来查了下主要存在的问题如下
- ThreadLocal可能内存泄露?
带着疑惑进入源码吧
源码
类声明和重要属性
package java.lang;
public class ThreadLocal<T> {
// hash值,类似于Hashmap,用于计算放在map内部数组的哪个index上
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);}
// 初始0
private static AtomicInteger nextHashCode = new AtomicInteger();
// 神奇的值,这个hash值的倍数去计算index,分布会很均匀,总之很6
private static final int HASH_INCREMENT = 0x61c88647;
static class ThreadLocalMap {
// 注意这是一个弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 初始容量16,一定要是2的倍数
private static final int INITIAL_CAPACITY = 16;
// map内部数组
private Entry[] table;
// 当前储存的数量
private int size = 0;
// 扩容指标,计算公式 threshold = 总容量 * 2 / 3,默认初始化之后为10
private int threshold;
增改操作
让我们先来看看增改方法
public void set(T value) {
Thread t = Thread.currentThread();
// 拿到当前Thread对象中的threadLocals引用,默认threadLocals值是null
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果ThreadLocalMap已经初始化过,就把当前ThreadLocal实例的引用当key,设置值
map.set(this, value); //下文详解
else
// 如果不存在就创建一个ThreadLocalMap并且提供初始值
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
让我们来看看map.set(this, value)具体怎么操作ThreadLocalMap
private void set(ThreadLocal<?> key, Object value) {
// 获取ThreadLocalMap内部数组
Entry[] tab = table;
int len = tab.length;
// 算出需要放在哪个桶里
int i = key.threadLocalHashCode & (len-1);
// 如果当前桶冲突了,这里没有用拉链法,而是使用开放定指法,index递增直到找到空桶,数据量很小的情况这样效率高
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 拿到目前桶中key
ThreadLocal<?> k = e.get();
// 如果桶中key和我们要set的key一样,直接更新值就ok了
if (k == key) {
e.value = value;
return;
}
// 桶中key是null,因为是弱引用,可能被回收掉了,这个时候我们直接占为己有,并且进行cleanSomeSlots,当前key附近局部清理其他key是空的桶
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 如果没冲突直接新建
tab[i] = new Entry(key, value);
int sz = ++size;
// 当前key附近局部清理key是空的桶,如果一个也没清除并且当前容量超过阈值了就扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private void rehash() {
// 这个方法会清除所有key为null的桶,清理完后size的大小会变小
expungeStaleEntries();
// 此时size还大于阈值的3/4就扩容
if (size >= threshold - threshold / 4)
// 2倍扩容
resize();
}
为什么会内存泄漏
总算读玩了set,大概明白了为什么会发生内存泄漏,画了个图

ThreadLocalMap.Entry中的key保存了ThreadLocal实例的一个弱引用,如果ThreadLocal实例栈上的引用断了,只要GC一发生,就铁定被回收了,此时Entry的key,就是null,但是呢Entry的value是强引用而且是和Thread实例生命周期绑定的,也就是线程没结束,值就一直不会被回收,所以产生了内存泄漏。
总算明白了,为什么一个set操作要这么多次清理key为null的桶。
既然这么麻烦,为什么key一定要用弱引用?
继续看上面的图,如果我们的Entry中保存的是ThreadLocal实例的一个强引用,我们删掉了ThreadLocal栈上的引用,同理此时不仅value就连key也不会回收了,这内存泄漏就更大了
查询操作
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();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果只是threadLocals.Entry是空,就设置value为null
map.set(this, value);
else
// 如果threadLocals是空,就new 一个key是当前ThreadLocal,value是空的ThreadLocalMap
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
让我们来看看map.getEntry(this)具体怎么操作ThreadLocalMap
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
// 最好情况,定位到了Entry,并且key匹配
return e;
else
// 可能是hash冲突重定址了,也可能是key被回收了
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 向后遍历去匹配key,同时清除key为null的桶
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
如何避免内存泄漏
新增,查询中无处不在的去清理key为null的Entry,是不是我们就可以放心了,大多数情况是的,但是如果我们在使用线程池,核心工作线程是不会停止的,会重复利用,这时我们的Entry中的value就永远不会被回收了这很糟糕,还好源码作者还没给我提供了remove方法,综上所述,养成良好习惯,只要使用完ThreadLocal,一定要进行remove防止内存泄漏
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 主要多了这一步,让this.referent = null,GC会提供特殊处理
e.clear();
expungeStaleEntry(i);
return;
}
}
}
Java读源码之ThreadLocal的更多相关文章
- Java读源码之ReentrantLock
前言 ReentrantLock 可重入锁,应该是除了 synchronized 关键字外用的最多的线程同步手段了,虽然JVM维护者疯狂优化 synchronized 使其已经拥有了很好的性能.但 R ...
- Java读源码之ReentrantLock(2)
前言 本文是 ReentrantLock 源码的第二篇,第一篇主要介绍了公平锁非公平锁正常的加锁解锁流程,虽然表达能力有限不知道有没有讲清楚,本着不太监的原则,本文填补下第一篇中挖的坑. Java读源 ...
- Java读源码之CountDownLatch
前言 相信大家都挺熟悉 CountDownLatch 的,顾名思义就是一个栅栏,其主要作用是多线程环境下,让多个线程在栅栏门口等待,所有线程到齐后,栅栏打开程序继续执行. 案例 用一个最简单的案例引出 ...
- Java读源码之Thread
前言 JDK版本:1.8 阅读了Object的源码,wait和notify方法与线程联系紧密,而且多线程已经是必备知识,那保持习惯,就从多线程的源头Thread类开始读起吧.由于该类比较长,只读重要部 ...
- Java读源码之Object
前言 JDK版本: 1.8 最近想看看jdk源码提高下技术深度(比较闲),万物皆对象,虽然Object大多native方法但还是很重要的. 源码 package java.lang; /** * Ja ...
- Java读源码之LockSupport
前言 JDK版本: 1.8 作用 LockSupport类主要提供了park和unpark两个native方法,用于阻塞和唤醒线程.注释中有这么一段: 这个类是为拥有更高级别抽象的并发类服务的,开发中 ...
- java读源码 之 map源码分析(HashMap,图解)一
开篇之前,先说几句题外话,写博客也一年多了,一直没找到一种好的输出方式,博客质量其实也不高,很多时候都是赶着写出来的,最近也思考了很多,以后的博客也会更注重质量,同时也尽量写的不那么生硬,能让大家 ...
- java读源码 之 queue源码分析(PriorityQueue,附图)
今天要介绍的是基础容器类(为了与并发容器类区分开来而命名的名字)中的另一个成员--PriorityQueue,它的大名叫做优先级队列,想必即使没有用过也该有所耳闻吧,什么?没..没听过?emmm... ...
- java读源码 之 list源码分析(LinkedList)
文章目录 LinkedList: 继承关系分析: 字段分析: 构造函数分析: 方法分析: LinkedList: 继承关系分析: public class LinkedList<E> ex ...
随机推荐
- Python---环境以及编辑器的使用的学习
1.搭建python的环境 官网下载Python的安装程序,记住一点在安装的时候点一下下面的 Add Python 3.5 to PATH 它会自动给你把安装的python的环境加入到计算机的环境变量 ...
- Django+Nginx概念安装和使用–使用Django建立你的第一个网站
一 前记 最近在使用Django倒腾属于自己的网站,由于以前没有接触过多少这类信息,所以,很多东西都是从零开始学习的.在参考网上的资料时候,发现很多对这方面记录的,很多人都写的不是很清楚,也许我这个新 ...
- 设置普通用户输入sudo,免密进入root账户
满足给开发用户开权限,赋予sudo权限.又不让其输入密码的方式: 方式一: 开始系统内部的wheel用户组, 在/etc/suoers 中编辑配置文件如下: %wheel ALL=(ALL) NOPA ...
- PHP如何解决表单重复提交
利用session 表单隐藏域中存放session(表单被请求时生成的标记).采用此方法在接收表单数据后,检查此标志值是否存在,先进行删除,然后处理数据; 若不存在,说明已提交过,忽略本次提交. ...
- springboot使用jdbcTemplate连接数据库
springboot使用jdbcTemplate连接数据库 1.pom.xml: <?xml version="1.0" encoding="UTF-8" ...
- Redis 的底层数据结构(SDS和链表)
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库.缓存和消息中间件.可能几乎所有的线上项目都会使用到 Redis,无论你是做缓存.或是用作消息中间件,用起来很简单方便 ...
- MySQL二进制日志分析-概述篇
MySQL从3.23版本开始引入了二进制日志,用于的数据复制, 二进制日志根据MySQL的版本不同,目前有4个版本: https://dev.mysql.com/doc/internals/en/bi ...
- C#中using的使用-以FileStream写入文件为例
场景 CS中FileStream的对比以及使用方法: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/100396022 关注公众号 ...
- Winform中实现ZedGraph曲线图的图像复制到剪切板、打印预览、获取图片并保存、另存为的功能
场景 Winforn中设置ZedGraph曲线图的属性.坐标轴属性.刻度属性: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/10 ...
- Java抽象类构造方法
java中抽象类的子类的构造方法会隐含父类的无参构造方法. package com.zempty.abstractclass; public class AbstractDemo01 { public ...