ThreadLocal是什么?一篇博客帮你搞定。

1. ThreadLocal是什么?

首先,学过操作系统的都应该知道,同一个进程中的线程之间的头是相互独立的,数据部分是共享的。现在如果我们想让每个线程都有自己的数据,从而实现线程数据隔壁,那么如何实现?ThreadLocal就是来做这个事情的,通过ThreadLocal可以给线程设置自己的局部变量,也可取出自己的局部变量。说白了,ThreadLocal就是想在多线程环境下去保证成员变量的安全。

另外,通过ThreadLocal的字面意思(线程的局部变量)也可以初步了解其作用。

注意:本文中说的线程的局部变量是指线程自己的数据,不是指方法中的局部变量。

2. ThreadLocal如何实现对线程局部变量的操作

2.1 分析

首先,我们查看一下ThreadLocal类中的源码:
在这里插入图片描述
以下是ThreadLocal类中的部分源码:

public class ThreadLocal<T> {

	ThreadLocal.ThreadLocalMap threadLocals = null;

	public T get() {
		//得到当前的线程对象
        Thread t = Thread.currentThread();
        
        //获取该线程的属性:ThreadLocalMap;ThreadLocalMap是用来存放该线程所有的局部变量的容器,是一个map结构
        ThreadLocalMap map = getMap(t);
        
        //如果map存在,则将当前ThreadLocal对象作为key,根据key获取内容
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

	public void set(T value) {
		//得到当前的线程对象
        Thread t = Thread.currentThread();
		
		//获取该线程的属性:ThreadLocalMap
        ThreadLocalMap map = getMap(t);

		//如果map存在,则以当前ThreadLocal对象为key,在map中设置key-value
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

	//获取该线程的成员变量:threadLocals
	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

	//ThreadLocalMap类,是ThreadLocal的内部类
	static class ThreadLocalMap {
	
		// Entry类,是ThreadLocalMap的内部类
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

		private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
				...
            tab[i] = new Entry(key, value);
            	...
        }
     }
   }

注意:如果以上源码看不懂没关系,接着往下看,我详细解释。


	public void set(T value) {
		//得到当前的线程对象
        Thread t = Thread.currentThread();
		
		//获取该线程的属性:ThreadLocalMap
        ThreadLocalMap map = getMap(t);

		//如果map存在,则以当前ThreadLocal对象为key,从Entry中设置key-value
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

	//获取该线程的属性:ThreadLocalMap
	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

getMap这个方法,我们可以得出结论,threadLocals是线程的一个成员变量。
set方法,我们可以知道,threadLocals是一个ThreadLocalMap类型的变量,并且以当前的ThreadLocal对象为key,线程要存放的局部变量为value存放于map中。

由此可见,线程的成员变量threadLocals就是用来存放线程的局部变量的容器,是一个map结构。为线程存放局部变量时,是以当前的ThreadLocal对象为key,要存放的局部变量为value存放数据的。另外,线程的局部变量是有线程对象管理的,而不是交给ThreadLocal管理的,因为threadLocals是线程对象的属性。


现在,我们知道了ThreadLocalMap是用来存放线程局部变量的容器,那么我们接下来来了解ThreadLocalMap:

	//ThreadLocalMap类,是ThreadLocal的内部类
	static class ThreadLocalMap {
	
		// Entry类,是ThreadLocalMap的内部类
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

		private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
				...
            tab[i] = new Entry(key, value);
            	...
        }
     }
   }

通过以上代码可知,ThreadLocalMapThreadLocal的内部类,Entry类是ThreadLocalMap的内部类。ThreadLocalMap的map结构实际上是通过Entry类型的数组实现的。

2.2 小结

  1. 每个Thread维护着一个存放线程局部变量的容器:ThreadLocalMap,其是一个map结构。
  2. ThreadLocalMapThreadLocal的内部类,其map结构是用Entry数组实现的,也就是数据最终存放在Entry对象中。
  3. 调用ThreadLocal的set()给线程设置局部变量时,实际上就是往ThreadLocalMap设置值,keyThreadLocal对象,值是传递进来的要设置局部变量。
  4. 调用ThreadLocalget()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象。
  5. ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

其关系,可以用如下一张图表示:
在这里插入图片描述

3. 举一个例子帮助理解

比如,我们在实现银行转账的时候,A给B转500元,具体步骤如下:

1. 从数据库中读取A的钱
2. 从数据库中读取B的钱
3. A的钱 - 500
4. B的钱 + 500
5. 将A现在的钱写入数据库
6. 将B现在的钱写入数据库

这6步中操作数据库时,应该用的是同一个Connection连接对象,因为这样能够保证这1,2,5,6操作数据库时如果有一个没有操作成功则整个6步都无效,从而保证了不会一方加钱,另一方不减钱的情况。

那么如何实现这6步中操作数据库时,应该用的是同一个Connection连接对象?
其实,我们只需要为每个线程对象的局部变量中存放同一个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"));

            //初始化线程本地
            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();
        }
    }


}

4. 内存泄露的问题

首先,简单介绍一下一些相关术语:

  1. 内存泄漏:指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

  2. 强引用:指如果一个对象=null, 但是该对象在被其他对象使用,则该对象不会被垃圾回收机制回收。

  3. 弱引用:指如果一个对象=null, 但是该对象在被其他对象使用,则该对象会被垃圾回收机制回收。

看如下一段源码:

		static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

可以看见,实际存储类Entry对ThreadLocal对象是弱引用关系,也就是ThreadLocalMap对于key是弱引用关系。

为什么要设置成若引用关系?
你可以这样想,如果ThreadLocal = null时,ThreadLocalMap中存放以ThreadLocal为key的键值对是否应该删除,ThreadLocal 是否应该删除???
答案显然是都应该删除。如果是强引用,则都不能删除,只有在该线程被回收时才都被删除。那么至少我们应该把能删除的删除了,弱引用关系能够将ThreadLocal删除,ThreadLocalMap中存放以ThreadLocal为key的键值对通过其他手段删除。

弱引用关系会造成,ThreadLocal = null时,ThreadLocal被回收,但是ThreadLocalMap中存放以ThreadLocal为key的键值对没有被回收,且无法被访问,这样就造成了内存泄漏,事实上早期是这样的,现在这个问题被解决了。

解决的方法就是:在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。当然,我们也可以手动的通过调用ThreadLocal的remove方法进行释放!

参考文件:
https://blog.csdn.net/qq_42862882/article/details/89820017
https://www.jianshu.com/p/ee8c9dccc953

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章