1.什么是threadlocal
什么是 ThreadLocal?
ThreadLocal 诞生于 JDK 1.2,用于解决多线程间的数据隔离问题。也就是说 ThreadLocal 会为每一个线程创建一个单独的变量副本。
一句话概括threadlocal
让当前线程有一个属于自己的变量 对其他线程隔离。
- 每个线程thread里面都有一个threadlocalMap,里面存放着这个线程的threadlocal -> value的映射关系。key就是threadlocal本身
- 每个线程可以设置多个threadlocal -> value的映射关系
- 多个线程之间的threadlocalmap互相独立。往ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。
- threadlocal的作用是防止改线程内的变量 被其他变量篡改
ThreadLocal 有什么用?
ThreadLocal 最典型的使用场景有两个:
- ThreadLocal 可以用来管理 Session,因为每个人的信息都是不一样的,所以就很适合用 ThreadLocal 来管理;
- 数据库连接,为每一个线程分配一个独立的资源,也适合用 ThreadLocal 来实现。
其中,ThreadLocal 也被用在很多大型开源框架中,比如 Spring 的事务管理器,还有 Hibernate 的 Session 管理等,既然 ThreadLocal 用途如此广泛,那接下来就让我们共同看看 ThreadLocal 要怎么用?ThreadLocal 使用中要注意什么?以及 ThreadLocal 的存储原理等,一起来看吧。
2.用法
基本用法
public class ThreadLocalTest {
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
// 存值
threadLocal.set(Arrays.asList("Java日知录", "Java 面试题"));
// 取值
List list = (List) threadLocal.get();
System.out.println(list.size());
System.out.println(threadLocal.get());
//删除值
threadLocal.remove();
System.out.println(threadLocal.get());
}
}
执行结果
> 2 [Java日知录, Java 面试题] null
数据共享
既然 ThreadLocal 设计的初衷是解决线程间信息隔离的,那 ThreadLocal 能不能实现线程间信息共享呢?
答案是肯定的,只需要使用 ThreadLocal 的子类 InheritableThreadLocal 就可以轻松实现,来看具体实现代码:
public class InheritableThreadLocalTest {
public static void main(String[] args) {
ThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
inheritableThreadLocal.set("Java日知录");
new Thread(() -> System.out.println(inheritableThreadLocal.get())).start();
}
}
输出结果
> Java日知录
/**
* threadlocal就是每个线程独有的一份变量 各个线程之间相互隔离 胡不干扰
* 同样的一个threadlocal 在不同的线程中调用 得到的变量是不一样的 因为调用get方法时 先获得当前线程的threadlocalmap(threadLocals), 然后再获得这个map中的threalocal(this) -> value
*/
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
private static ThreadLocal<Map<String, String>> threadLocal2 = new InheritableThreadLocal<>(); // 线程间可见的threadlocal
public static void main(String args[]) {
// 设置
threadLocal.set("这是在主线程中");
System.out.println("线程名字:" + Thread.currentThread().getName() + "---" + threadLocal.get());
HashMap<String, String> map = new HashMap<>();
map.put("jam","3");
threadLocal2.set(map);
//线程a
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程名字:" + Thread.currentThread().getName() + "---" + threadLocal.get()); // 为空 因为每个线程的threadlocal是独立的
threadLocal.set("这是在线程a中");
System.out.println("线程名字:" + Thread.currentThread().getName() + "---" + threadLocal.get());
threadLocal.remove();
}
}, "线程a").start();
//线程b
new Thread(() -> {
System.out.println("获取主线程中的threadlocal2中的jam---" + Thread.currentThread().getName() + "---" + threadLocal2.get().get("jam"));
}, "线程b").start();
}
输出结果
线程名字:main---这是在主线程中
线程名字:线程a---null
线程名字:线程a---这是在线程a中
获取主线程中的threadlocal2中的jam---线程b---3
高级用法 内存泄漏
public class ThreadLocalOomTest {
static ThreadLocal threadLocal = new ThreadLocal();
static Integer MOCK_MAX = 10000;
static Integer THREAD_MAX = 100;
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_MAX);
for (int i = 0; i < THREAD_MAX; i++) {
executorService.execute(() -> {
threadLocal.set(new ThreadLocalOomTest().getList());
System.out.println(Thread.currentThread().getName());
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
executorService.shutdown();
}
List getList() {
List list = new ArrayList();
for (int i = 0; i < MOCK_MAX; i++) {
list.add("Version:JDK 11");
list.add("ThreadLocal");
list.add("Author:Java日知录");
list.add("DateTime:" + LocalDateTime.now());
list.add("Test:ThreadLocal OOM");
}
return list;
}
}
设置 JVM(Java 虚拟机)启动参数 -Xmx=100m (最大运行内存 100 M),运行程序不久后就会出现如下异常:
此时我们用 VisualVM 观察到程序运行的内存使用情况,发现内存一直在缓慢地上升直到内存超出最大值,从而发生内存溢出的情况。
内存使用情况,如下图所示:
内存泄漏的原因
其中:实线代表强引用,虚线代表弱引用(弱引用具有更短暂的生命周期,在执行垃圾回收时,一旦发现只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存)。
看到这里我们就理解了 ThreadLocal 造成内存溢出的原因:如果 ThreadLocal 没有被直接引用(外部强引用),在 GC(垃圾回收)时,由于ThreadLocalMap 中的 key 是弱引用,所以一定就会被回收,这样一来 ThreadLocalMap 中就会出现 key 为 null 的Entry,并且没有办法访问这些数据,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 并且永远无法回收,从而造成内存泄漏。
正确的用法
既然已经知道了 ThreadLocal 内存溢出的原因,那解决办法就很清晰了,只需要在使用完 ThreadLocal 之后,调用remove()方法,清除掉 ThreadLocalMap 中的无用数据就可以了。
正确使用的完整示例代码如下:
public class ThreadLocalOomTest2 {
static ThreadLocal threadLocal = new ThreadLocal();
static Integer MOCK_MAX = 10000;
static Integer THREAD_MAX = 100;
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_MAX);
for (int i = 0; i < THREAD_MAX; i++) {
executorService.execute(() -> {
threadLocal.set(new ThreadLocalOomTest2().getList());
System.out.println(Thread.currentThread().getName());
// 移除对象
threadLocal.remove();
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
executorService.shutdown();
}
List getList() {
List list = new ArrayList();
for (int i = 0; i < MOCK_MAX; i++) {
list.add("Version:JDK 8");
list.add("ThreadLocal");
list.add("Author:Java日知录");
list.add("DateTime:" + LocalDateTime.now());
list.add("Test:ThreadLocal OOM");
}
return list;
}
}
可以看出核心代码,我们添加了一句 threadLocal.remove() 命令就解决了内存溢出的问题,这个时候运行代码观察,发现内存的值一直在一个固定的范围内
3.原理
set方法 把自己作为一个key 去map中塞入对应的value
public void set(T value) {
// 获取当前的线程
Thread t = Thread.currentThread();
// 获取线程里的threadlocalmap
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value); // 获取map里的key对应的value key就是threadlocal本身
} else {
createMap(t, value);
}
}
private void set(ThreadLocal<?> key, Object value) {
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) { // 相等 直接替换
e.value = value;
return;
}
if (k == null) { // 为空 则塞入key value
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
get方法 把自己作为一个key 去map中获取对应的value
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;
}
}
return setInitialValue();
}
从整个代码可以看出,首先 ThreadLocal 并不存储数据,而是依靠 ThreadLocalMap 来存储数据,ThreadLocalMap 中有一个 Entry 数组,每个 Entry 对象是以 K/V 的形式对数据进行存储的,其中 K 就是 ThreadLocal 本身,而 V就是要存储的值,如下图所示:
原理总结
- ThreadLocalMap里面有个静态内部类Entry, key是ThreadLocal, value是Object
- ThreadLocalMap 是 threadLocal中的一个静态内部类。
- 每个Thread都有一个属性叫TreadLocal.ThreadLocalMap
- ThreadLocal的set方法,实际上就是往ThreadLocalMap里面设置值,key为ThreadLocal value位要存储的值
- get方法就是从ThreadLocalMap里面取值,key为ThreadLocal
4.常见应用场景
管理connection
最典型的是管理数据库的Connection:当时在学JDBC的时候,为了方便操作写了一个简单数据库连接池,需要数据库连接池的理由也很简单,频繁创建和关闭Connection是一件非常耗费资源的操作,因此需要创建数据库连接池~
那么,数据库连接池的连接怎么管理呢??我们交由ThreadLocal来进行管理。为什么交给它来管理呢??ThreadLocal能够实现 当前线程的操作都是用同一个Connection,保证了事务!
public class DBUtil {
//数据库连接池
private static BasicDataSource source;
//为不同的线程管理连接
private static ThreadLocal<Connection> local;
static {
try {
//加载配置文件
Properties properties = new Properties();
//获取读取流
InputStream stream = DBUtil.class.getClassLoader().getResourceAsStream("连接池/config.properties");
//从配置文件中读取数据
properties.load(stream);
//关闭流
stream.close();
//初始化连接池
source = new BasicDataSource();
//设置驱动
source.setDriverClassName(properties.getProperty("driver"));
//设置url
source.setUrl(properties.getProperty("url"));
//设置用户名
source.setUsername(properties.getProperty("user"));
//设置密码
source.setPassword(properties.getProperty("pwd"));
//设置初始连接数量
source.setInitialSize(Integer.parseInt(properties.getProperty("initsize")));
//设置最大的连接数量
source.setMaxActive(Integer.parseInt(properties.getProperty("maxactive")));
//设置最长的等待时间
source.setMaxWait(Integer.parseInt(properties.getProperty("maxwait")));
//设置最小空闲数
source.setMinIdle(Integer.parseInt(properties.getProperty("minidle")));
//初始化线程本地
local = new ThreadLocal<>();
} catch (IOException e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
if(local.get()!=null){
return local.get();
}else{
//获取Connection对象
Connection connection = source.getConnection();
//把Connection放进ThreadLocal里面
local.set(connection);
//返回Connection对象
return connection;
}
}
//关闭数据库连接
public static void closeConnection() {
//从线程中拿到Connection对象
Connection connection = local.get();
try {
if (connection != null) {
//恢复连接为自动提交
connection.setAutoCommit(true);
//这里不是真的把连接关了,只是将该连接归还给连接池
connection.close();
//既然连接已经归还给连接池了,ThreadLocal保存的Connction对象也已经没用了
local.remove();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
避免重复参数传递
避免一些参数的传递的理解可以参考一下Cookie和Session:
- 每当我访问一个页面的时候,浏览器都会帮我们从硬盘中找到对应的Cookie发送过去。
- 浏览器是十分聪明的,不会发送别的网站的Cookie过去,只带当前网站发布过来的Cookie过去
浏览器就相当于我们的ThreadLocal,它仅仅会发送我们当前浏览器存在的Cookie(ThreadLocal的局部变量),不同的浏览器对Cookie是隔离的(Chrome,Opera,IE的Cookie是隔离的【在Chrome登陆了,在IE你也得重新登陆】),同样地:线程之间ThreadLocal变量也是隔离的....
那上面避免了参数的传递了吗??其实是避免了。Cookie并不是我们手动传递过去的,并不需要写<input name= cookie/>来进行传递参数...
在编写程序中也是一样的:日常中我们要去办理业务可能会有很多地方用到身份证,各类证件,每次我们都要掏出来很麻烦
// 咨询时要用身份证,学生证,房产证等等....
public void consult(IdCard idCard,StudentCard studentCard,HourseCard hourseCard){
}
// 办理时还要用身份证,学生证,房产证等等....
public void manage(IdCard idCard,StudentCard studentCard,HourseCard hourseCard) {
}
//......
// 咨询时要用身份证,学生证,房产证等等....
public void consult(){
threadLocal.get();
}
// 办理时还要用身份证,学生证,房产证等等....
public void takePlane() {
threadLocal.get();
}
而如果用了ThreadLocal的话,ThreadLocal就相当于一个机构,ThreadLocal机构做了记录你有那么多张证件。用到的时候就不用自己掏了,问机构拿就可以了。
在咨询时的时候就告诉机构:来,把我的身份证、房产证、学生证通通给他。在办理时又告诉机构:来,把我的身份证、房产证、学生证通通给他。...
参考链接
本文仅供个人学习使用 侵删