并发面试题--什么是ThreadLocal本地局部变量?

本文作者:黄海燕,叩丁狼高级讲师。原创文章,转载请注明出处。

ThreadLocal是一个本地线程副本变量工具类,可以理解为线程的本地变量。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,实现了线程的数据隔离,是线程独有的所以本身就是线程安全,特别适用于各个线程依赖多个不同的变量值完成操作的场景。如:数据库连接、Session管理等。

##1.1 ThreadLocal简单使用
###1.1.1 ThreadLocal常用方法:
image.png
###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内存分析:
image.png
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存在问题
image.png

我们可以通过源代码发现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冲突问题

image.png
问题原因:当发生元素存在Hash相同时,容器中发生Hash冲突,在HashMap中采用的是同槽位链表的方式解决的,就是如果hash值相同,将冲突的所有元素放到同一个链表中,而ThreadLocalMap是以线性探测的方式,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。通过上图红色部分发现,ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。ThreadLocalMap采用线性探测的方式如果出现hash冲突需要不停的计算,知道找到为止,这样解决Hash冲突的效率很低。如果出现很多ThreadLocal对象放入到Map中,hash出现冲突的几率就大,性能将大大降低。
解决方案:一个线程尽量少使用多个ThreadLocal对象。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/wolfcode_cn/article/details/87428353
今日推荐