浅谈Java中的ThreadLocal

​ 之前用过很多次TreadLocal,但是对其的理解并不是很深刻,通过查阅资料和阅读数据对它进一步的了解,谈一谈我对TreadLocal的理解,探讨一下使用方法和实现原理

一.对ThreadLocal的理解

ThreadLocal叫做本地变量,也可以叫做线程本地存储,它为变量在每个线程中创建一个副本,那么每个线程就可以访问自己内部的副本变量,即使两个线程同时执行这段代码,他们也无法访问到对象的ThreadLocal变量,本质上实现数据的隔离,避免不同线程之间访问变量产生冲突。

理解误区

需要强调的是,不能使用ThreadLocal和Synchronized进行比较,二者没有比较意义,synchronized是一种互斥的同步机制,为了保证多线程情况下对共享资源的正确访问,然而ThreadLocal本质是提供一种线程的变量作用域,他是一种线程封闭技术(每个线程独享变量),ThreadLocal可以理解为将对象的作用范围限制在一个线程上下文中,使得变量的作用域为“线程级”。

没有ThreadLocal的时候,一个线程在其声明周期内,可能穿过多个层级,多个方法,如果有个对象需要在此线程周期内多次调用,且是跨层级的(线程内共享),通常的做法是通过参数进行传递;而ThreadLocal将变量绑定在线程上,在一个线程周期内,无论“你身处何地”,只需通过其提供的get方法就可轻松获取到对象。极大地提高了对于“线程级变量”的访问便利性。

二、ThreadLocal简单使用

假设我们要为每个线程关联一个唯一的序号,在每个线程周期内,我们需要多次访问这个序号,这时我们就可以使用ThreadLocal了

/**
 * @ClassName:ThreadLocalTest
 * @Description: ThreadLocal测试
 * @Author: yaoyonghao
 * @Date: 2021/4/22、13:52
 */
public class ThreadLocalTest {
    public static void main(String[] args) {
        for (int index = 0; index < 5; index++) {
            final Thread thread = new Thread() {

                @Override
                public void run() {
                    System.out.println("当前线程是:" + Thread.currentThread().getName() + "被分配的线程id:" + GetThreadId.get());
                }
            };
            thread.start();
        }
    }

    static class GetThreadId {
        //给每个线程分配一个线程id,使用AtomicInteger保证线程安全
        private static final AtomicInteger nextThreadId = new AtomicInteger(0);
        private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
            /**
             * 相当于nextId++,由于nextId++这种操作是个复合操作而非原子操作,
             * 会有线程安全问题(可能在初始化时就获取到相同的ID,所以使用原子变量
             * @return
             */
            protected Integer initialValue() {
                return nextThreadId.getAndIncrement();
            }
        };

        ///返回当前线程的唯一的序列,如果第一次get,会先调用initialValue
        public static int get() {
            return threadId.get();
        }
    }
}

执行结果

当前线程是:Thread-0被分配的线程id:0
当前线程是:Thread-1被分配的线程id:1
当前线程是:Thread-2被分配的线程id:2
当前线程是:Thread-3被分配的线程id:3
当前线程是:Thread-4被分配的线程id:4

三、深入理解ThreadLocal源码

先了解一下ThreadLocal类提供的几个方法

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }

get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,

set()用来设置当前线程中变量的副本,

remove()用来移除当前线程中变量的副本,

initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法

get()方法

    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();
    }

第一句是取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是 this,而不是当前线程t。如果获取成功,则返回value值。 如果map为空,则调用setInitialValue方法返回value。

看看其中getMap方法

   ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
 //ThreadLocalMap是内部静态类
 static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

,在getMap中,是调用当期线程t,返回当前线程t中的一个成员变量threadLocals。其中ThreadLocalMap是ThreadLocal的静态内部类。

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

ThreadLocal是如何为每个线程创建变量的副本的:

首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

四、ThreadLocal实际中如何使用

1、connection管理

**最典型的是管理数据库的Connection:**当时在学JDBC的时候,为了方便操作写了一个简单数据库连接池,需要数据库连接池的理由也很简单,频繁创建和关闭Connection是一件非常耗费资源的操作,因此需要创建数据库连接池,Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理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 {
        //获取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();
        }
    }
}

五、ThreadLocal经常遇到的内存泄露问题

ThreadLocal一直set会存在内存泄露问题
ThreadLocal都会存储在内部类ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。,其中有段代码如下:

/**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

首先解释一下弱引用:

​ 只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。

按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了

解决方案

在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了。

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("张三");
    ……
} finally {
    localName.remove();
}

参考文章如下

Java并发编程:深入剖析ThreadLocal
Java中ThreadLocal的实际用途是啥?
谈谈Java中的ThreadLocal

**

关注我的微信公众号

**

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值