Java并发编程:ThreadLocal类

ThreadLocal的作用

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?

JDK 中自带的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

ThreadLocal类型的变量,每个线程访问的时候,都会有这个变量的本地副本。线程可以使用get和set方法来获取默认值或将其值更改为当前线程所存的副本值,避免线程安全问题。

使用案例

使用ThreadLocal存放SimpleDateFormat类型的变量

java 复制代码
import java.text.SimpleDateFormat;
import java.util.Random;

public class ThreadLocalExample implements Runnable{

     // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for(int i=0 ; i<10; i++){
            Thread t = new Thread(obj, ""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        //初始格式是:yyyyMMdd HHmm
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //formatter pattern is changed here by thread, but it won't reflect to other threads
        // 默认构造方法,格式是yy-M-d ah:mm
        formatter.set(new SimpleDateFormat());

        System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
    }

}

执行结果

java 复制代码
Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter = yy-M-d ah:mm
Thread Name= 1 default Formatter = yyyyMMdd HHmm
Thread Name= 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 formatter = yy-M-d ah:mm
Thread Name= 3 default Formatter = yyyyMMdd HHmm
Thread Name= 2 formatter = yy-M-d ah:mm
Thread Name= 4 default Formatter = yyyyMMdd HHmm
Thread Name= 3 formatter = yy-M-d ah:mm
Thread Name= 4 formatter = yy-M-d ah:mm
Thread Name= 5 default Formatter = yyyyMMdd HHmm
Thread Name= 5 formatter = yy-M-d ah:mm
Thread Name= 6 default Formatter = yyyyMMdd HHmm
Thread Name= 6 formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yyyyMMdd HHmm
Thread Name= 7 formatter = yy-M-d ah:mm
Thread Name= 8 default Formatter = yyyyMMdd HHmm
Thread Name= 9 default Formatter = yyyyMMdd HHmm
Thread Name= 8 formatter = yy-M-d ah:mm
Thread Name= 9 formatter = yy-M-d ah:mm

结果分析

formatter初始格式是:yyyyMMdd HHmm,在线程执行过程中,会将其修改为yy-M-d ah:mm。线程0将其修改为yy-M-d ah:mm格式之后,线程1获取到的formatter仍然是:yyyyMMdd HHmm,说明线程0修改了格式,不影响线程1,线程0只是修改其本地副本。

不使用ThreadLocal

java 复制代码
import java.text.SimpleDateFormat;
import java.util.Random;

public class ThreadLocalExamlple implements Runnable {

    //private static final ThreadLocal<SimpleDateFormat> formatter =
    //        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    private static SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd HHmm");

    public void run() {
        System.out.println("Thread Name= " + Thread.currentThread().getName()+
                " default Formatter = " +formatter.toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        formatter = new SimpleDateFormat();

        System.out.println("Thread Name= " + Thread.currentThread().getName() + " formatter " + formatter.toPattern());
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExamlple obj = new ThreadLocalExamlple();
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(obj, "" + i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }
}

执行结果

ini 复制代码
Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter yy-M-d ah:mm
Thread Name= 1 default Formatter = yy-M-d ah:mm
Thread Name= 2 default Formatter = yy-M-d ah:mm
Thread Name= 1 formatter yy-M-d ah:mm
Thread Name= 3 default Formatter = yy-M-d ah:mm
Thread Name= 2 formatter yy-M-d ah:mm
Thread Name= 4 default Formatter = yy-M-d ah:mm
Thread Name= 3 formatter yy-M-d ah:mm
Thread Name= 5 default Formatter = yy-M-d ah:mm
Thread Name= 4 formatter yy-M-d ah:mm
Thread Name= 6 default Formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yy-M-d ah:mm
Thread Name= 6 formatter yy-M-d ah:mm
Thread Name= 5 formatter yy-M-d ah:mm
Thread Name= 7 formatter yy-M-d ah:mm
Thread Name= 8 default Formatter = yy-M-d ah:mm
Thread Name= 8 formatter yy-M-d ah:mm
Thread Name= 9 default Formatter = yy-M-d ah:mm
Thread Name= 9 formatter yy-M-d ah:mm

结果分析

formatter格式是:yyyyMMdd HHmm,在线程执行过程中,会将其修改为yy-M-d ah:mm。线程0将其修改为yy-M-d ah:mm格式之后,后面线程获取到的初始formatter是:yy-M-d ah:mm,说明线程0修改了格式,不是在其本地修改的,其他线程能获取到线程0修改之后的值。

ThreadLocal的原理

ThreadLocal的set方法源码

java 复制代码
public void set(T value) {
    //获取当前请求的线程
    Thread t = Thread.currentThread();
    //取出 Thread 类内部的 threadLocals 变量(哈希表结构)
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 将需要存储的值放入这个哈希表中
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

根据上述代码可以看到:最终的变量其实是存放在当前线程的ThreadLocalMapThreadLocal中的静态内部类)中,而不是存在ThreadLocal`上

ThreadLocal可以理解为只是ThreaLocalMap的封装,传递了变量值。ThreadLocal类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)访问到该线程的ThreadLocalMap对象。

Thread类

java 复制代码
public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}

每个Thread对象中都只有一个ThreadLocalMapThreadLocalMap可以存储以ThreadLocal为key,Object对象为value的键值对。

比如我们在同一个线程中声明了两个ThreaddLocal对象的话,Thread内部都是使用仅有的那个ThreadLocalMap存放数据的,ThreaLocalMap的key就是ThreadLocal对象,value就是ThreadLocal对象调用set方法设置的值。

ThreadLocal 数据结构如下图所示

ThreadLocal 内部类

ThreadLocal的内存泄漏问题

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。

这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后最好手动调用remove()方法

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

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

弱引用介绍

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

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

参考文章

  1. JavaGuide面试题
相关推荐
刘大辉在路上4 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
测试老哥5 小时前
外包干了两年,技术退步明显。。。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
追逐时光者6 小时前
免费、简单、直观的数据库设计工具和 SQL 生成器
后端·mysql
初晴~6 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱581366 小时前
InnoDB 的页分裂和页合并
数据库·后端
小_太_阳6 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾7 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala
ThisIsClark7 小时前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
星就前端叭7 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc