基本概念

ThreadLocal叫做线程变量,意思是ThreadLocal对象中填充的变量属于当前线程,通过该变量可以为当前线程设置它的独立副本,该副本对其他线程而言是隔离的,每个线程只能访问自己内部的副本

数据结构

案例代码

@Data
@Slf4j
public class ThreadDemo {
    private String content;

    /**
     * 用于存储线程id
     */
    private static ThreadLocal threadIdCache = new ThreadLocal();

    /**
     * 用于存储线程name
     */
    private static ThreadLocal threadNameCache = new ThreadLocal();



    public static void main(String[] args) throws InterruptedException {

        ThreadDemo threadDemo = new ThreadDemo();

        for (int i = 0; i < 3; i++) {

            new Thread(new Runnable() {
                @Override
                public void run() {
                   threadIdCache.set(Thread.currentThread().getId());
                   threadNameCache.set(Thread.currentThread().getName());
                   log.info("ThreadId:{}, ThreadName:{}", threadIdCache.get(), threadNameCache.get());
                   threadIdCache.get();
                }
            }).start();
        }
    }
}

打印内容
在这里插入图片描述
代码图解(直接看比较模糊,建议在新标签页打开图片)
在这里插入图片描述

温馨提示
需要案例代码 + 案例代码图解 + 阅读源码(最好自己debug下)才能真正掌握

set方法源码

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

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

线程私有性举例

案例一

@Data
@Slf4j
public class ThreadDemo {
    private String content;
    
    public static void main(String[] args) throws InterruptedException {

        ThreadDemo threadDemo = new ThreadDemo();
        
        for (int i = 0; i < 5; i++) {
           
            new Thread(new Runnable() {
                @Override
                public void run() {
                    // 写threadDemo
                    threadDemo.setContent("线程:" + Thread.currentThread().getName() + "设置值");
                    // 读threadDemo
                    System.out.println("线程:" +Thread.currentThread().getName() +  "获取值" + threadDemo.getContent());
                }
            }).start();
        }
    }
}

因为局部变量threadDemo是这五个线程所共享的变量,因为for循环执行5次的时间可近乎忽略,所有上述5个线程是并发执行的,并发读写threadDemo就会出现如下读写不一致的现象
在这里插入图片描述
案例二:使用threadLocal 优化后就线程安全了

public class ThreadDemo {

    private static ThreadLocal<String> threadLocal = new ThreadLocal();

    private String content;

    public void setContent(String content) {
        threadLocal.set(content);
    }

    public String getContent() {
        return threadLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {

        ThreadDemo threadDemo = new ThreadDemo();
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
           
                @Override
                public void run() {
                    threadDemo.setContent("线程:" + Thread.currentThread().getName() + "设置值");
                    System.out.println("线程:" +Thread.currentThread().getName() +  "获取值" + threadDemo.getContent());
                }
            }).start();
        }
    }
}

虽然我们再案例一中,使用threadDemo作为线程对象锁也可以达到同样的效果,如下
在这里插入图片描述
但区别在于

  • synchronized是用于线程间的数据共享,本质是给共享变量threadDemo 加锁来解决的,例如上述我们对相关代码块加锁后,这5个线程,只能一个个按序去访问,也就是的并行去修改共享变量threadDemo,以时间换空间并发性低
  • threadLocal 是用于线程间的数据隔离, 是通过为每一个线程提供一份线程副本来实现线程间数据隔离的,此时我们这5个线程在并发访问的都是他们自己线程副本,自然不会存在线程安全问题,通过这种机制来保证线程间数据隔离,以空间换时间并发性强

内存安全

ThreadLocal中的remove方法用于释放该threadLocal对象相关的线程的内存,只要在threadLocal使用完毕后及时释放,就不会出现线程问题,源码如下

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
     }

继续以上面的ThreadDemo为例,做一个关于threadIdCache这个静态对象引用的内存图,绿色的虚线是重点关注的地方,意味着threadLocal类型的key是以虚引用的形式存储在threadlocalMap中的,关于弱引用、强引用的概念本文不过多叙述,但要学习如下内容是必不可少的,推荐参考文章:JAVA基础 - 强引用、弱引用、软引用、虚引用
在这里插入图片描述

分析Threadlocal的内存泄漏要结合两个因素,即Threadlocal与Thread的使用情况

首先分析Thread,如果thread线程是作为局部变量使用,那么thread生命周期会非常短,在线程执行完毕就会结束,thread被回收,即便我们使用完threadlocal没有remove也不会发生内存泄漏,如图
在这里插入图片描述
但实际开发中,我们接触的通常都是使用线程池管理的多线程环境,例如使用springboot做web开发,这种情况下线程的生命周期会伴随整个程序的运行周期

在考虑使用线程池的前提下,假如我们线程池有500个线程,Threadlocal的使用分两种情况

Threadlocal是作为局部变量
有一个方法,里面会创建一个threadlocal,并通过set为当前线程关联一个线程变量,用该线程池执行这个接口/方法100万次,其中会累计创建了100万个threadlocal对象,每个线程平均被关联了2000个线程变量

threadlocal是作为局部变量,方法结束threadlocal的(强)引用出栈,只剩线程Thread中的ThreadLocalMap中某个使用threadlocal(弱)引用作为key的Entry,所以下次GC时堆中的threadlocal对象被GC回收,该entry的key最终指向的是一个null,最终变为entry(null,value),这这个entry对应的value永远无法访问到,出现内存泄漏,测试demo

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadLocalTest {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(500);
        for (int i = 0; i < 1000000; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
                    threadLocal.set((int)(Math.random() * 1000));
                    System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get());
                    // 不执行 remove
                }
            });
        }
        
        executorService.shutdown();
        while (!executorService.isTerminated()) {
            // 等待所有任务完成
        }
        
        System.out.println("所有任务已完成");
    }
}

但按我们的计算,真的每个线程平均被绑定了2000个变量,累计出现100万个entry(null,value)吗?这就要提到ThreadLocal另一兜底机制,在我们没有及时remove的情况下,会导致出现大量entry(null,value),ThreadLocal 的 set() 方法在创建新的 Entry 时,如果 ThreadLocalMap 的大小(也即是已使用的 Entry 的数量)超过了阈值(默认为容量的2/3),就会调用 expungeStaleEntries() 方法清理所有键为空的 Entry。防止内存泄漏导致OOM

Threadlocal是作为静态变量
如果ThreadLocal是静态变量,那么它的生命周期就会伴随整个程序运行的生命周期,那么方法区中,就存在一个ThreadLocal是静态引用,这样无论多少次GC,threadlocal对象都不会被回收,所以不存在entry(null,value)这种情况
在这里插入图片描述
所以这种情况会不会导致比较严重的内存泄漏呢?

首先,如果是作为静态变量,threadLocal数量是有限的,不会像作为局部变量的时候,可能在程序运行期间累计创建成百上千万个,我们这里撑死算10个吧,线程池的数据500个左右,那么即便是没有及时remove,那么最多会有5000个无效的entry(1个线程关联10个threadlocal),整体来说是内存泄漏程度是可接受的

假如threadlocal这个key是作为强引用存储在threadLocalMap中呢?
那就会存在内存泄漏导致内存溢出的风险,还是接着上面那个例子,threadlocal是作为局部变量,方法结束threadlocal的(强)引用出栈,此时还剩线程Thread中的ThreadLocalMap中某个使用threadlocal引用作为key的Entry,假如这里也是强引用,那么即便是栈中的threadlocal的强引用出栈了,依然还是有ThreadLocalMap中某个Entry的强引用指向它,这导致threadlocal和这个Entry均无法被回收

如果是程序运行过程中,threadlocal作为局部变量可能被创建成百上千万次,这样在程序运行过程中会内存泄漏最终导致OOM

总结

threadlocal内存安全机制还是比较安全的,为我们提供了比较健全的兜底机制,但尽管如此我们也要重视内存的主动释放,且确保释放代码放到finally代码块中,尽可能避免内存泄露问题。

常见场景

线程安全应用 - Spring事务

/*
1 要保证所有的操作要么都成功要么都失败? => 使用数据库事物
2 如何保证全部操作都在同一事物内? => 所有的操作都必须使用同一个connection链接对象
3 如何保证所有的操作都必须使用同一个connection链接对象?=> 首先将这些操作通常是位于同一个线程内的 
=> 只需要为一个线程绑定一个数据库连接对象就可
4 用ThreadLocal来防止一个线程获取多个数据库连接对象 (因为ThreadLocal是线程独立的),过程参考下面代码
 */
class JDBCUtils {
    //-----------------------------------------------Druid链接池返回链接对象------------------------------------------------
    private static DataSource dlSource = null;
    //形式是:默认的隐形key threadName => conn;
    private static ThreadLocal<Connection> conns = new ThreadLocal<Connection>();
    //以静态代码块的形式创建dlSource链接池资源
    static {
        Properties pros = new Properties();
        //读取druid配置文件,内容无非是一些数据库链接使用的url,username,password...
        InputStream is = JDBCUtils.class.getClassLoader().getResourceAsStream("Druid.properties");
        try {
            pros.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            dlSource = DruidDataSourceFactory.createDataSource(pros);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static Connection getDlConnection() {

        //判断当前线程是否已经关联了一个连接对象(意味着这个线程之前已经获取到链接了)
        Connection conn = conns.get();
        //如果是null,意味着当前线程还没有创建过数据库连接对象,此时我们要去连接池中去获取链接然后保存到conns中
        //否则,就意味着当前线程已有数据库连接对象,直接返回之前创建的就行了
        if(conn == null){
            try {
                conn = dlSource.getConnection();
                conns.set(conn);//为线程A保存刚刚获取的数据库连接对象conn
                conn.setAutoCommit(false);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return conn;
    }
}

数据跨层传递,线程安全的本地缓存

Threadlocal本质是作为一个map容器,虽然它本身并不持有map对象,但该类的核心api都是对ThradLocalMap操作,所以Threadlocal经常是被拿来做本地缓存的,比如我项目中的拦截器,用于做用户鉴权,功能类似于网关,鉴权完毕,需要把一部分用户信息(比如访客id)缓存起来,并在service层取出使用。那么可以优先考虑Threadlocal,当前线程会绑定唯一一份当前访客的用户信息

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐