网站显示系统建设中,wix和wordpress区别,做网站时怎样图片上传怎么才能让图片不变形有什么插件吗,肯尼亚网站域名提示#xff1a;ThreadLocal详解、ThreadLocal与synchronized的区别、ThreadLocal的优势、ThreadLocal的内部结构、ThreadLocalMap源码分析、ThreadLocal导致内存泄漏的原因、要避免内存泄漏可以用哪些方式、ThreadLocal怎么解决Hash冲突问题、避免共享的设计模式、ThreadLoca… 提示ThreadLocal详解、ThreadLocal与synchronized的区别、ThreadLocal的优势、ThreadLocal的内部结构、ThreadLocalMap源码分析、ThreadLocal导致内存泄漏的原因、要避免内存泄漏可以用哪些方式、ThreadLocal怎么解决Hash冲突问题、避免共享的设计模式、ThreadLocal的场景面试题、并发编程的相关设计 文章目录 前言一、ThreadLoacl介绍1、什么是ThreadLocal2、基本使用3、使用案例3.1、不使用ThreadLocal(存在并发问题)3.2、使用ThreadLocal 4、ThreadLocal与synchronized的区别4.1、synchronized代码4.2、ThreadLocal代码 5、ThreadLocal的优势6、ThreadLocal在spring事务中的应用 二、底层原理1、ThreadLocal的内部结构2、这样设计的好处3、ThreadLocalMap源码分析4、ThreadLocal怎么解决Hash冲突问题(源码角度)4.1、threadLocal的set方法源码4.2、threadLocal的set方法 三、ThreadLocal的场景面试题1、ThreadLocal导致内存泄漏的原因2、那明明是弱引用也会有内存泄漏的问题为何还要用弱引用呢3、要避免内存泄漏可以用哪些方式 四、避免共享的设计模式1、不变性模式1.1、概念2、不变性模式的好处3、如何实现不可变性 2、写时复制模式3、线程本地存储模式(Thread-Specific Storag) 总结 前言
ThreadLocal是并发编程中的一块内容 工作中也有一定的使用频率今天与大家一起探讨一下ThreadLocal的源码同时也为自己以后的查阅提供一些资料。本人水平有限如有误导欢迎斧正一起学习共同进步 一、ThreadLoacl介绍
1、什么是ThreadLocal
官方文档ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get、set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的用于关联线程和线程上下文。 特性
线程安全在多线程并发的场景下保证线程安全传递数据我们可以通过ThreadLocal在同一线程不同组件中传递公共变量线程隔离每个线程的变量都是独立的不会互相影响。
2、基本使用
在使用前我们先来认识几个ThreadLocal的常用方法
方法声明描述ThreadLocal()创建ThreadLocal对象public void set(T value)设置当前线程绑定的局部变量public T get()获取当前线程绑定的局部变量public void remove()移除当前线程绑定的局部变量
3、使用案例
3.1、不使用ThreadLocal(存在并发问题)
因为是5个线程操作同一个共享变量(content)所以会有并发问题。
package com.zheng.test.threadLocak;/*** author: ztl* date: 2024/07/07 23:08* desc:*/
public class ThreadLocalDemo {private String content;private String getContent() {return content;}private void setContent(String content) {this.content content;}public static void main(String[] args) {ThreadLocalDemo demo new ThreadLocalDemo();for (int i 0; i 5; i) {Thread thread new Thread(new Runnable() {Overridepublic void run() {demo.setContent(Thread.currentThread().getName() 的数据);System.out.println(Thread.currentThread().getName() --- demo.getContent());}});thread.setName(线程 i);thread.start();// 执行结果
// 线程0---线程1的数据
// 线程3---线程3的数据
// 线程1---线程1的数据
// 线程4---线程4的数据
// 线程2---线程2的数据}}
}
3.2、使用ThreadLocal
使用ThreadLocal就不会有并发问题了代码
public class ThreadLocalDemo2 {private static ThreadLocalString threadLocal new ThreadLocal();private String content;private String getContent() {return threadLocal.get();}private void setContent(String content) {threadLocal.set(content);}public static void main(String[] args) {ThreadLocalDemo2 demo new ThreadLocalDemo2();for (int i 0; i 5; i) {Thread thread new Thread(new Runnable() {Overridepublic void run() {demo.setContent(Thread.currentThread().getName() 的数据);System.out.println(Thread.currentThread().getName() --- demo.getContent());}});thread.setName(线程 i);thread.start();// 执行结果
// 线程0---线程0的数据
// 线程3---线程3的数据
// 线程1---线程1的数据
// 线程4---线程4的数据
// 线程2---线程2的数据}}
}
4、ThreadLocal与synchronized的区别
synchronized是独占锁相当于串行执行了那多线程就没啥意义了 你执行完毕才到我。 而ThreadLocal是并发执行效率更高.、
synchronizedthreadLocal原理同步机制采用以时间换空间的方式只提供了一份变量让不同的线程排队访问采用“以空间换时间”的方式为每个线程都提供了一份变量的副本从而实现同时访问而互不干扰侧重点多个线程之间访问公共资源的同步多线程中让每个线程之间的数据互相隔离
4.1、synchronized代码
synchronized代码如下
public class ThreadLocalDemo3 {private String content;private String getContent() {return content;}private void setContent(String content) {this.content content;}public static void main(String[] args) {ThreadLocalDemo3 demo new ThreadLocalDemo3();long start System.currentTimeMillis();for (int i 0; i 5; i) {Thread thread new Thread(new Runnable() {Overridepublic void run() {synchronized (demo){demo.setContent(Thread.currentThread().getName() 的数据);try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}long end System.currentTimeMillis();System.out.println(Thread.currentThread().getName() --- demo.getContent()耗时(end - start) 毫秒);}});thread.setName(线程 i);thread.start();}}
}执行结果 线程0—线程4的数据耗时2013毫秒 线程4—线程4的数据耗时4019毫秒 线程3—线程3的数据耗时6026毫秒 线程2—线程2的数据耗时8034毫秒 线程1—线程1的数据耗时10041毫秒
4.2、ThreadLocal代码
ThreadLocal代码如下
public class ThreadLocalDemo4 {private static ThreadLocalString threadLocal new ThreadLocal();private String content;private String getContent() {return threadLocal.get();}private void setContent(String content) {threadLocal.set(content);}public static void main(String[] args) {ThreadLocalDemo4 demo new ThreadLocalDemo4();long start System.currentTimeMillis();for (int i 0; i 5; i) {Thread thread new Thread(new Runnable() {Overridepublic void run() {demo.setContent(Thread.currentThread().getName() 的数据);try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}long end System.currentTimeMillis();System.out.println(Thread.currentThread().getName() --- demo.getContent()耗时(end - start) 毫秒);}});thread.setName(线程 i);thread.start();}}
}执行结果就是 线程4—线程4的数据耗时2004毫秒 线程0—线程0的数据耗时2004毫秒 线程2—线程2的数据耗时2004毫秒 线程3—线程3的数据耗时2004毫秒 线程1—线程1的数据耗时2004毫秒
5、ThreadLocal的优势
在一些特定的场景下ThreadLocal有两个突出的优势
传递数据保存每个线程绑定的数据有需要的地方可以直接获取避免参数直接传递带来的代码耦合问题线程隔离各线程之间的数据相互隔离又具备并发性避免同步方式带来的性能损失(比如上面的synchronized要比ThreadLocal慢的多)
6、ThreadLocal在spring事务中的应用
spring的事务就借助了ThreadLocal类。spring会从数据库连接池中获的一个connection然后把connection放进ThreadLocal中也就和线程绑定了事务需要提交或者回滚只要从ThreadLocal中拿到connection进行操作。 为何spring的事务要借助ThreadLocal类 以下面的代码为例主要有3个步骤 事务准备阶段1-3行 业务处理阶段 4-6行 事务提交阶段 7行 很明显不管是事务的开启、执行具体的sql、提交事务、都是需要数据库连接(connction)的。如果我们把控制事务的代码放在dao层那肯定是需要一个字段比如说connectionObject字段来存储这个connection对象毕竟我们都是需要一个数据库连接的(如何让三个DAO使用同一个数据源连接呢我们就必须为每个DAO传递同一个数据库连接要么就是在DAO实例化的时候作为构造方法的参数传递要么在每个DAO的实例方法中作为方法的参数传递。)。那我们如果只调用一个dao层这样还算合适。那如果我们要连续操作3个dao呢30个dao呢难道要这30个dao每个dao中都加一个connectionObject字段然后把这30个dao中挨着传递下去这个connection连接吗毕竟我们一定是要基于同一个connection连接来操作的。那要保证这30个dao一定是同一个connection那肯定要把这个connection传递下去如果纯代码实现的话那必然要加一个字段然后挨着传递下去的。这样实现肯定是不如直接把connection存在线程的threadlocal中然后直接从线程的threadlocal中获取这样也不用传递还能获得同一个connection还没有并发问题肯定是比代码硬写要优美的多的。
1 dbc new DataBaseConnection();//第1行
2 Connection con dbc.getConnection();//第2行
3 con.setAutoCommit(false);// //第3行
4 con.executeUpdate(...);//第4行
5 con.executeUpdate(...);//第5行
6 con.executeUpdate(...);//第6行
7 con.commit();第7行web容器中每个完整的请求周期都会由一个线程来处理。因此如果我们能把一些参数用threadlocal绑定到线程的话就可以实现参数共享(隐形共享)结合spring中的ioc和aop就可以很好的解决这一点。只要将一个数据库连接放到threadLocal中当前线程执行时直接从threadLocal中获取是比较方便的。
二、底层原理
1、ThreadLocal的内部结构
如果我们不去看源码的话可能以为ThreadLocal是这么设计的创建一个map其中map的key是threadv是要存的内容。jdk早期版本的threadLocal确实是这么设计的但现在早已不是了(因为有各种缺点也就是后面设计方案的优点)。
在jdk8中ThreadLocal的设计是每个Thread都维护一个自己的ThreadLocalMap这个map的key是ThreadLocal实例本身value是真正要存储的值Object具体的过程:
每个Thread线程内部都有一个Map(ThreadLocalMap)Map中存储ThreadLocal对象(key)和线程的变量副本(value)Thread内部的Map是由ThreadLocal维护的由ThreadLocal负责像map获取和设置线程的变量值对于不同的线程每次获取副本时别的线程并不能获取到当前线程的副本值形成了副本的隔离互补干扰。
2、这样设计的好处
threadLocal存的数据(entity)会变小。同时防止了大的对象。按最开始的设计方案我有一个大的Mapkey是线程v是数据。那来一个线程我就得存一个线程再来一个线程我在存第二个。。那100个线程1000个线程我同时存1000份的key、value那这个对象也太大了。然后存的entity也太多了。但是如果分开的话是每个线程都有自己独立的threadLocal最起码不会有大的Map对象。而且我用到了threadLocal才有不用就没有。减少内存的使用。还有一个好处就是我存储到Thread内部的话一旦Thread销毁了那么对应的ThreadLocalMap也会随之销毁。能减少内存的使用。毕竟如果用原始的设计的话你这个thread在threadLocalmap中添加了添加的时候key是threadv是数据。那你这个thread一旦被销毁了我threadLocalMap可不会销毁毕竟我又不是只有你一个thread我还有别的thread呢我总不能因为你死了我就直接全部销毁。我还得专门去销毁你这个单独的thread不然就浪费资源我要是单独消耗也好消耗我的性能那还真不如是在thread内部添加上threadLocalMap对象一旦thread消耗了那threadLocalMap也自动销毁了。
3、ThreadLocalMap源码分析
ThreadLocalMap是ThreadLocal的内部类没有实现Map接口用独立的方式实现了Map功能其内部的entry也是独立实现。 entry是真正存储数据的entry继承了WeakReference并用ThreadLocal作为key。其目的是将threadLocal对象的生命周期和线程声明周期解绑 强引用就是我们最常见的普通对象引用只要还有强引用指向一个对象就能表名对象还“活着”垃圾回收器就不会回收这种对象 弱引用(WeakReference)垃圾回收器一旦发现了只具有弱引用的对象不管当前内存空间是否足够都会回收他的内存。
4、ThreadLocal怎么解决Hash冲突问题(源码角度)
threadLocal是通过线性探测法来解决hash冲突的。可以通过阅读源码来验证
4.1、threadLocal的set方法源码
public void set(T value) {Thread t Thread.currentThread();ThreadLocalMap map getMap(t);if (map ! null)map.set(this, value);elsecreateMap(t, value);
}ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}void createMap(Thread t, T firstValue) {t.threadLocals new ThreadLocalMap(this, firstValue);
}ThreadLocalMap(ThreadLocal? firstKey, Object firstValue) {table new Entry[INITIAL_CAPACITY];int i firstKey.threadLocalHashCode (INITIAL_CAPACITY - 1);table[i] new Entry(firstKey, firstValue);size 1;setThreshold(INITIAL_CAPACITY);
}这个就是单纯的说如果map不为空则将参数设置到map中如果map为空则创建map并设置初始值。
int i firstKey.threadLocalHashCode (INITIAL_CAPACITY - 1); 的意思是说求模来获取在map中的巢(其实就是map中的数组对应的位置)。就比如说101 % 16任何数对16取模结果一定是0-15的数据也就是为了确定是存到哪个节点中的。setThreshold(INITIAL_CAPACITY); 这个点进去就是threshold len * 2 / 3; 意思是当占了2/3的空间以后就开始扩容了。
4.2、threadLocal的set方法
map.set(this, value);(上面代码块的5行)源码
private void set(ThreadLocal? key, Object value) {// We dont use a fast path as with get() because it is at// least as common to use set() to create new entries as// it is to replace existing ones, in which case, a fast// path would fail more often than not.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) {replaceStaleEntry(key, value, i);return;}}tab[i] new Entry(key, value);int sz size;if (!cleanSomeSlots(i, sz) sz threshold)rehash();
}上面这段的意思简单的说就是
没元素直接插入有元素判断key相等不相等不相等就往后移一位看有元素没没元素直接插入有元素key相等直接覆盖 e tab[i nextIndex(i, len)]) { 上面代码中的这行点击去就是
private static int nextIndex(int i, int len) {return ((i 1 len) ? i 1 : 0);
}也就是说他是个环形数组 如果数组的当前位置1到达了数组的最后那么就返回0(数组的第一个位置)如果数组的当前位置1还没达到数组的最后那么就往后移动一位。也就是这是一个收尾相连的数组。从这里我们可以看出来他是通过线性探测法去解决hash冲突的。毕竟冲突了以后他往后移动一位继续比较有位置就塞进去没位置就继续往后走可不就是线性探测法嘛。
三、ThreadLocal的场景面试题
1、ThreadLocal导致内存泄漏的原因
从下面的图可以看出来 当前线程指向mapmap指向entryentry中的key指向ThreadLocalentry的v指向自己的my value。一旦触发了垃圾回收key与ThreadLocal之间的引用是弱引用直接回收了但map跟entry之间是强引用entry的v和v的内容之间也是强引用(v和v的内容的强引用是导致内存泄漏的真实原因)。其实要想内存泄漏必须满足两点
没有手动删除这个entry当前线程(currentThread)仍在执行。因为我们很可能使用线程池之类的技术导致这个线程一直都没结束。 总结一下ThreadLocal内存泄漏的根源是由于ThreadLocalMap的生命周期和Thread一样长如果没有手动删除对应的key就会导致内存泄漏。 那怎么解决这种内存泄漏呢每次用完threadLocal都给remove掉对应的threadLocal就行。
2、那明明是弱引用也会有内存泄漏的问题为何还要用弱引用呢
如果是弱引用的话是只有entry中的v会有内存泄漏一旦我用强引用的话我entry中的key也会有内存泄漏的问题的。能优化一点是一点肯定是用弱引用啊。而且ThreadLocalMap中的set/getEntry方法中会对key为null(也就是ThreadLocal为null就是弱引用被gc掉的那个)进行判断如果为null的话那么会对value置为null的。比用强引用来说多一层保障弱引用的threadLocal会被回收对应的value在下一次ThreadLocalMap调用get、set、remove任一方法的时候会被清除。
3、要避免内存泄漏可以用哪些方式
使用完ThreadLocal调用其remove方法删除对应的entry使用完threadLocal当前的thread也随之运行结束。
相比于第一种方式第二种方式更不好控制特别是使用线程池的时候线程结束是不会被销毁的因此更推荐使用方式1remove掉对应的entry。
四、避免共享的设计模式
不变性模式、写时复制模式、线程本地存储模式都可以避免共享。
使用时需要注意不变形模式的属性不可变性写时复制模式需要注意拷贝的性能问题线程本地存储模式需要注意异步执行问题
1、不变性模式
1.1、概念
多个线程同时读写同一个共享变量会存在并发问题。同时读写才会有问题。那我只能读不能写就没这个问题了。不变性模式是一种创建不可变对象的设计模式。即对象一旦创建成功后就不能在修改。在多线程下使用不可变对象可以避免线程安全问题并提高程序的性能和可读性。
2、不变性模式的好处
线程安全不可变对象在多线程环境下不需要同步操作可以避免线程安全问题可读性不可变对象在创建后不可修改可以更加清晰的表达对象的含义和作用性能由于不可变对象的状态不可变可以进行更有效的缓存和优化操作可测试性不可变对象对单元测试非常友好可以更容易的进行测试和验证。
3、如何实现不可变性
不可变性模式的主要思想是通过将对象的状态设置为final和私有并提供只读方法来保证对象的不可变性。在创建不可变对象时需要确保对象的所有属性都是不可变的在创建后不会被修改同时还需要注意不可变对象间的引用关系以避免出现对象的状态变化。 jdk中很对类都具备不可变性例如Sting和Long、Integer、Double 等基础类型的包装类都具备不可变性他们都遵循了不可变类的三点要求类和属性都是final的所有方法均是只读的。
2、写时复制模式
该模式的基本思想是在共享数据被修改时先将数据复制一份然后对副本进行修改最后再讲副本替换为原始的共享数据。 不可变对象的写操作往往都是通过使用Copy-on-Write方法解决的当然Copy-on-Write不局限于这个模式。Copy-on-Write才是最简单的并发解决方案他是如此的简单以至于Java中的数据类型String、Integer、Long 都是基于Copy-on-Write方案实现的。 Copy-on-Write的缺点是消耗内存。每次修改都需要复制一个新的对象好在随着gc算法的成熟这种内存消耗渐渐可以接收了。在实际工作中如果读多写少的场景可以使用Copy-on-Write。
3、线程本地存储模式(Thread-Specific Storag)
该模式的基本思想的为每个线程创建独立的存储空间用于存储线程私有的数据。ThreadLocal类实现了该模式。 如果你在并发场景中使用一个线程不安全的工具最简单的方案就是避免共享。避免共享有两种方案一种方案是将这个类作为局部变量使用另一种方案就是线程本地存储模式。局部变量的方案的缺点是在高并发场景下会频繁创建对象而现场本地存储方案每个线程值需要创建一个工具类的实例所以不存在频繁创建对象的问题
线程本地存储通常用于以下场景
保存上下文信息在多线程环境中每个线程都有自己的执行上下文包括线程的状态、环境变量、运行时状态等。管理线程安全对象在多线程环境中共享对象通常需要同步操作以避免竞争条件但是有些对象多线程安全的么可以被多个线程同时访问而不需要同步操作。线程本地存储可以用来管理这些线程安全对象使得每个线程都可以独立的访问自己的对象实例而不需要进行同步操作。实现线程的特定的行文优先应用程序需要再每个线程中执行特定的行为比如记录日志、统计数据、授权访问等。线程本地存储可以实现这个线程的特定行为而不需要跟其他线程进行协调 总结
有时候我们阅读源码不单单是为了看他的源码是啥样的更多的是给我们扩展了视野以后工作中遇到类似的问题可能有一个新的解决方案或是方向来帮助我们更好更快的解决各种问题。