本文作者:黄海燕,叩丁狼高级讲师。原创文章,转载请注明出处。
ThreadLocal是一个本地线程副本变量工具类,可以理解为线程的本地变量。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,实现了线程的数据隔离,是线程独有的所以本身就是线程安全,特别适用于各个线程依赖多个不同的变量值完成操作的场景。如:数据库连接、Session管理等。
1.1 ThreadLocal简单使用
1.1.1 ThreadLocal常用方法:
1.1.2 需求:简易数据库连接池
因为数据库连接Connection的创建和消耗非常浪费资源,所以我们就选择了数据库连接池,以空间换时间,事前创建一定数量的数据库连接,存放到一个池子中,有需要的时候拿来用,用完放回放回到池子中。并且在多线程中很可能一个线程在使用Connection进行数据库操作,而另外一个线程调用同一个Connection的close方法关闭链接。
public class DBUtil {
//数据库连接池
private static DataSource source;
//为了保证同一个线程使用的连接操作和释放都是同一个Connection,需要使用ThreadLocal
private static ThreadLocal<Connection> local = new ThreadLocal<Connection>();
private static LinkedList<Connection> pool = new LinkedList<Connection>();
private static int initSize = 10;
static {
try {
Class.forName("com.mysql.jdbc.Driver");
for (int i = 0;i<initSize;i++){
Connection a = DriverManager.getConnection("jdbc:mysql://localhost:3306/shop?useSSL=false","root","admin");
pool.addLast(a);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
//获取Connection对象
Connection connection = pool.getFirst();
//把Connection放进ThreadLocal里面
local.set(connection);
//返回Connection对象
return connection;
}
//关闭数据库连接
public static void closeConnection() {
//从线程中拿到Connection对象
Connection connection = local.get();
if (connection != null) {
//放回去,连接池
pool.addLast(connection);
//既然连接已经归还给连接池了,ThreadLocal保存的Connction对象也已经没用了
local.remove();
}
}
}
注意:再多线程中为了保证每个线程的连接和释放的对象一致,需要使用ThreadLocal来保证连接的线程和释放的线程是同一个。
1.2 源码分析
ThreadLocal内存分析:
set方法源码分析:
public void set(T value) {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//获取当前线程中独有的ThreadLocalMap对象
if (map != null)//map不为空
//将key=当前ThreadLocal对象和value=传入的对象,放入到线程不共享的ThreadLocalMap中
map.set(this, value);
else
createMap(t, value);//为空就创建ThreadLocalMap再存值
}
get方法源码分析:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//拿到线程中独有的ThreadLocalMap对象
if (map != null) {
//通过当前的ThreadLocal对象做为key拿到存放在线程中的ThreadLocalMap中的元素
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {//如果元素不为空
T result = (T)e.value;//获取元素中的value,拿到之前存放在线程中的值
return result;
}
}
return setInitialValue();//如果拿不到结果,设置初始化值
}
1.3 ThreadLocal存在问题
我们可以通过源代码发现ThreadLocalMap中并不是实现了Map接口,而是自己写的一个Map结构。就是这个ThreadLocalMap存在两个问题,内存泄露无法释放内存和Hash冲突导致的运行效率低。
1.3.1 ThreadLocal的内存泄露
问题原因:上图,我们可以看到Entry中是继承了WeakReference类,WeakReference类是一个弱引用,生命周期只能存活到下次GC前,Entry构造器中发现只有key调用了父类构造器,也就是只规定了key为弱类型和key只能是ThreadLocal对象,并没有规定value,所以有可能导致key被GC回收了为null,而value还在ThreadLocalMap中被引用着,ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露,造成了资源的浪费。
解决方案:调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除
1.3.2 ThreadLocal的Hash冲突问题
问题原因:当发生元素存在Hash相同时,容器中发生Hash冲突,在HashMap中采用的是同槽位链表的方式解决的,就是如果hash值相同,将冲突的所有元素放到同一个链表中,而ThreadLocalMap是以线性探测的方式,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。通过上图红色部分发现,ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。ThreadLocalMap采用线性探测的方式如果出现hash冲突需要不停的计算,知道找到为止,这样解决Hash冲突的效率很低。如果出现很多ThreadLocal对象放入到Map中,hash出现冲突的几率就大,性能将大大降低。
解决方案:一个线程尽量少使用多个ThreadLocal对象。
想获取更多技术干货,请前往叩丁狼官网:http://www.wolfcode.cn/all_article.html