专栏首页汤圆学JavaJava并发:ThreadLocal的简单介绍
原创

Java并发:ThreadLocal的简单介绍

作者:汤圆

个人博客:javalover.cc

前言

前面在线程的安全性中介绍过全局变量(成员变量)和局部变量(方法或代码块内的变量),前者在多线程中是不安全的,需要加锁等机制来确保安全,后者是线程安全的,但是多个方法之间无法共享

而今天的主角ThreadLocal,就填补了全局变量和局部变量之间的空白

简介

ThreadLocal的作用主要有二:

  1. 线程之间的数据隔离:为每个线程创建一个副本,线程之间无法相互访问
  2. 传参的简化:为每个线程创建的副本,在单个线程内是全局可见的,在多个方法之间不需要传来传去

其实上面的两个作用,归根到底都是副本的功劳,即每个线程单独创建一个副本,就产生了上面的效果

ThreadLocal直译为线程本地变量,巧妙地融合了全局变量和局部变量两者的优点

下面我们分别举两个例子来说明它的作用

目录

  1. 例子 - 数据隔离
  2. 例子 - 传参优化
  3. 内部原理

正文

我们在接触一个新东西时,首先应该是先用起来,然后再去探究内部原理

Thread Local的使用还是比较简单的,类似Map,各种put/get

它的核心方法如下:

  • public void set(T value):保存当前副本到ThreadLocal中,每个线程单独存放
  • public T get():取出刚才保存的副本,每个线程只会取出自己的副本
  • protected T initialValue():初始化副本,作用和set一样,不过initialValue会自动执行,如果get()为空
  • public void remove():删除刚才保存的副本

1. 例子 - 数据隔离

这里我们用SimpleDateFormat举例,因为这个类是线程不安全的(后面有空再单独开篇),如果不做隔离,会有各种各样的并发问题

我们先来看下线程不安全的例子,代码如下:

public class ThreadLocalDemo {
​
    // 线程不安全:在多个线程中执行时,有可能解析出错
    private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    public void parse(String dateString){
        try {
            System.out.println(simpleDateFormat.parse(dateString));
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
​
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadLocalDemo demo = new ThreadLocalDemo();
        for (int i = 0; i < 30; i++) {
            service.execute(()->{
                demo.parse("2020-01-01");
            });
        }
    }
}

多次运行,可能会出现下面的报错:

Exception in thread "pool-1-thread-4" java.lang.NumberFormatException: empty String

关于SimpleDateFormat的不安全问题,在源码注释里有提到,如下:

Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

意思就是建议多线程使用时,要么每个线程单独创建,要么加锁

下面我们分别用加锁和单独创建来解决

线程安全的例子:加锁

public class ThreadLocalDemo {
​
    // 线程安全1:加内置锁
    private SimpleDateFormat simpleDateFormatSync = new SimpleDateFormat("yyyy-MM-dd");
    public void parse1(String dateString){
        try {
           synchronized (simpleDateFormatSync){
               System.out.println(simpleDateFormatSync.parse(dateString));
           }
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
​
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadLocalDemo demo = new ThreadLocalDemo();
        for (int i = 0; i < 30; i++) {
            service.execute(()->{
                demo.parse1("2020-01-01");
            });
        }
    }
}

线程安全的例子:通过ThreadLocal为每个线程创建一个副本

public class ThreadLocalDemo {
​
    // 线程安全2:用ThreadLocal创建对象副本,做数据隔离
    // 下面这个代码可以简化,通过 withInitialValue
    private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(){
        // 初始化方法,每个线程只执行一次;比如线程池有10个线程,那么不管运行多少次,总的SimpleDateFormat副本只有10个
        @Override
        protected SimpleDateFormat initialValue() {
            // 这里会输出10次,分别是每个线程的id
            System.out.println(Thread.currentThread().getId());
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
    public void parse2(String dateString){
        try {
            System.out.println(threadLocal.get().parse(dateString));
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
​
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadLocalDemo demo = new ThreadLocalDemo();
        for (int i = 0; i < 30; i++) {
            service.execute(()->{
                demo.parse2("2020-01-01");
            });
        }
    }
}

有的朋友可能会有疑问,这个例子为啥不直接创建局部变量呢?

这是因为如果创建局部变量,那么调用一次就会创建一个SimpleDateFormat,性能会比较低

而通过ThreadLocal为每个线程创建一个副本,那么基于这个线程的后续所有操作,都是访问这个副本,无需再次创建

2. 例子 - 传参优化

有时候,我们需要在多个方法之间进行传参(比如用户信息),此时就面临一个问题:

  • 如果将要传递的参数设置为全局变量,那么线程不安全
  • 如果将要传递的参数设置为局部变量,那么传参会很麻烦

这时就需要用到ThreadLocal了,正如开篇讲得,它的作用就是融合全局和局部的优点,使得线程也安全,传参也方便

下面是例子:

public class ThreadLocalDemo2 {
​
    // 参数传递,程序繁琐
    public void fun1(int age){
        System.out.println(age);
        fun2(age);
    }
    private void fun2(int age){
        System.out.println(age);
        fun3(age);
    }
    private void fun3(int age){
        System.out.println(age);
    }
​
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadLocalDemo2 demo = new ThreadLocalDemo2();
        for (int i = 0; i < 30; i++) {
            final int j = i;
            service.execute(()->{
                demo.fun1(j);
            });
        }
    }
}

这段代码可能没有实际意义,但是意思应该到了,就是表达传递参数的繁琐性

下面我们看下用ThreadLocal来解决这个问题

public class ThreadLocalDemo2 {
​
    // 简化,ThreadLocal当全局变量来使用
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
    public void fun11(){
        System.out.println(threadLocal.get());
        fun22();
    }
    private void fun22(){
        System.out.println(threadLocal.get());
        fun33();
    }
    private void fun33(){
        int age = threadLocal.get();
        System.out.println(age);
    }
​
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadLocalDemo2 demo = new ThreadLocalDemo2();
        for (int i = 0; i < 30; i++) {
            final int j = i;
            service.execute(()->{
                try{
                    threadLocal.set(j);
                    demo.fun11();
                }finally {
                    threadLocal.remove();
                }
            });
        }
    }
}

可以看到,这里我们不再把age参数传来传去,而是为每个线程创建一个副本age

这样所有方法都可以访问到副本,同时也保证了线程安全

不过要注意的是,这次的使用和上次不同,这次多了remove方法,它的作用就是删除上面set的副本,这个下面再介绍

3. 内部原理

先来说说它是怎么做到数据隔离

我们先来看下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);
}

可以看到,值是存在map里的(key是ThreadLocal对象,value就是为线程单独创建的副本)

而这个map是怎么来的呢?再来看下面的代码

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

可以看到,最终还是回到了Thread里面,这就是为啥线程之间实现了隔离,而线程内部实现了共享(因为是线程内的属性,只有当前线程可见)

我们再看下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();
}

可以看到,先找到当前线程内的map,然后再根据key取出value

最后一行的setInitialValue,就是在get为空时,重新执行的初始化动作

为什么要用ThreadLocal作为key,而不是线程id呢

是为了存储多个变量

如果用了线程id作为key,那么map里一个线程只能存放一个变量

而用了ThreadLocal作为key,那么可以一个线程存放多个变量(通过创建多个ThreadLocal)

如下所示:

private static ThreadLocal<Integer> threadLocal1 = new ThreadLocal<Integer>();
private static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<Integer>();
​
public void test(){
    threadLocal1.set(1);
    threadLocal2.set(2);
    System.out.println(threadLocal1.get());
    System.out.println(threadLocal2.get());
}

再来说下它的内存泄漏问题

我们先来看下ThreadLocalMap内部代码:

static class ThreadLocalMap {
​
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
​
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

可以看到,内部节点Entry继承了弱引用(在垃圾回收时,如果一个对象只有弱引用,则会被回收),然后在构造函数中通过super(k)将key设置为弱引用

因此在垃圾回收时,如果外部没有指向ThreadLocal的强引用,那么就会直接把key回收掉

此时key=null,而value还在,但是又取不出来,久而久之,就会出现问题

解决办法就是remove,通过在finally中remove,将副本从ThreadLocal中删除,此时key和value都被删除

总结

  1. ThreadLocal直译为线程本地变量,它的作用就是通过为每个线程单独创建一个副本,来保证线程间的数据隔离和简化方法间的传参
  2. 数据隔离的本质:Thread内部持有ThreadLocalMap对象,创建的副本都是存在这里,所以每个线程之间就实现了隔离
  3. 内存泄漏的问题:因为ThreadLocalMap中的key是弱引用,所以垃圾回收时,如果key指向的对象没有强引用,那么就会被回收,此时value还存在,但是取不出来,时间长了,就有问题(当然如果线程退出,那value还是会被回收)
  4. 使用场景:面试等场合

参考内容:

后记

其实这里没有很深入地去解析源码部分知识,主要是精力和能力有限,后面再慢慢深入吧

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java并发学习之ThreadLocal使用及原理介绍

    ThreadLocal使用及原理介绍 线程本地变量,每个线程保存变量的副本,对副本的改动,对其他的线程而言是透明的(即隔离的) 1. 使用姿势一览 使用方式也...

    一灰灰blog
  • -1-0 Java 简介 java是什么 java简单介绍

    了解 Java 技术  https://www.java.com/zh_CN/about/

    noteless
  • (82) 理解ThreadLocal / 计算机程序的思维逻辑

    本节,我们来探讨一个特殊的概念,线程本地变量,在Java中的实现是类ThreadLocal,它是什么?有什么用?实现原理是什么?让我们接下来逐步探讨。 基本概...

    swiftma
  • Java并发编程之美

    并发编程相比 Java 中其他知识点的学习门槛较高,从而导致很多人望而却步。但无论是职场面试,还是高并发/ 高流量系统的实现,却都离不开并发编程,于是能够真正掌...

    加多
  • 揭秘ThreadLocal

    ThreadLocal是开发中最常用的技术之一,也是面试重要的考点。本文将由浅入深,介绍ThreadLocal的使用方式、实现原理、内存泄漏问题以及使用场景。...

    大闲人柴毛毛
  • Java中的ThreadLocal功能演示

    除了使用synchronized同步符号外,Java中的ThreadLocal是另一种实现线程安全的方法。在进行性能测试用例的编写过程中,比较简单的办法就是直接...

    FunTester
  • java反射机制简单介绍

    gfu
  • 美团面试问ThreadLocal,学妹一口气给他说了四种!

    ThreadLocal类顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal, 每个线程往这个ThreadLocal中读写是线程隔离,互...

    java金融
  • RethinkDB的简单介绍

    RethinkDB最早是作为一个对SSD进行专门优化的MySQL存储引擎出现的,其特点在于对SSD的充分利用。而目前RethinkDB已经脱离MySQL成为一个...

    Debian中国
  • Serverless的简单介绍

    Serverless架构是近年来迅速兴起的一个技术概念。基于这种架构能构建出多种应用场景,适用于各行各业。只要是对轻计算、高弹性、无状态等场景有诉求,您都可以通...

    javascript.shop
  • 01_Vue的简单介绍

    兮动人
  • ThreadLocal 学习之路

    在并发编程中时常有这样一种需求:每条线程都需要存取一个同名变量,但每条线程中该变量的值均不相同。

    砍鸡鸡
  • java并发之无同步方案-ThreadLocal

    1.ThreadLocal 介绍2.ThreadLocal 应用3.ThreadLocal 源码解析3.1解决 Hash 冲突4.ThreadLocal ...

    Java宝典
  • ThreadLocal源码完美解读

    ThreadLocal源码解读,网上面早已经泛滥了,大多比较浅,甚至有的连基本原理都说的很有问题,包括百度搜索出来的第一篇高访问量博文,说ThreadLocal...

    好好学java
  • ThreadLocal 源码解读

    ThreadLocal源码解读,网上面早已经泛滥了,大多比较浅,甚至有的连基本原理都说的很有问题,包括百度搜索出来的第一篇高访问量博文,说ThreadLocal...

    java思维导图
  • FastThreadLocal 是什么鬼?吊打 ThreadLocal 的存在!!

    ThreadLocal 大家都知道是线程本地变量,今天栈长再介绍一个神器:FastThreadLocal,从字面上看就是:Fast + ThreadLocal,...

    Java技术栈
  • JAVA高逼格面试:线程封闭

    码农的世界从来不缺乏名词。如果没有,我们就强行弄上几个。这些名词有垂直领域的知识缩写,也有水平领域的抽象划分。有的行云流水无比顺畅,有的晦涩难懂如便秘。

    xjjdog
  • 一文搞懂 ThreadLocal 原理

    当多线程访问共享可变数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要线程封闭出场了。

    武培轩
  • Java进阶(七)正确理解Thread Local的原理与适用场景

    Jason Guo

扫码关注云+社区

领取腾讯云代金券