Java并发:ThreadLocal的简单介绍
作者:汤圆
个人博客:javalover.cc
前言
前面在线程的安全性中介绍过全局变量(成员变量)和局部变量(方法或代码块内的变量),前者在多线程中是不安全的,需要加锁等机制来确保安全,后者是线程安全的,但是多个方法之间无法共享
而今天的主角ThreadLocal,就填补了全局变量和局部变量之间的空白
简介
ThreadLocal的作用主要有二:
-
线程之间的数据隔离:为每个线程创建一个副本,线程之间无法相互访问
-
传参的简化:为每个线程创建的副本,在单个线程内是全局可见的,在多个方法之间不需要传来传去
其实上面的两个作用,归根到底都是副本的功劳,即每个线程单独创建一个副本,就产生了上面的效果
ThreadLocal直译为线程本地变量,巧妙地融合了全局变量和局部变量两者的优点
下面我们分别举两个例子来说明它的作用
目录
- 例子 - 数据隔离
- 例子 - 传参优化
- 内部原理
正文
我们在接触一个新东西时,首先应该是先用起来,然后再去探究内部原理
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)); } ad8 } 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 Thr 56c eadLocalDemo2 { // 参数传递,程序繁琐 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) { 56c 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都被删除
总结
- ThreadLocal直译为线程本地变量,它的作用就是通过为每个线 56c 程单独创建一个副本,来保证线程间的数据隔离和简化方法间的传参
- 数据隔离的本质:Thread内部持有ThreadLocalMap对象,创建的副本都是存在这里,所以每个线程之间就实现了隔离
- 内存泄漏的问题:因为ThreadLocalMap中的key是弱引用,所以垃圾回收时,如果key指向的对象没有强引用,那么就会被回收,此时value还存在,但是取不出来,时间长了,就有问题(当然如果线程退出,那value还是会被回收)
- 使用场景:面试等场合
参考内容:
- 《实战Java高并发》
- 廖雪峰ThreadLocal:https://www.liaoxuefeng.com/wiki/1252599548343744/1306581251653666
后记
其实这里没有很深入地去解析源码部分知识,主要是精力和能力有限,后面再慢慢深入吧
- 简单介绍了Java并发
- Java多线程并发技术,ThreadLocal介绍
- Java并发学习之ThreadLocal使用及原理介绍
- [Java 并发] Java并发编程实践 思维导图 - 第一章 简单介绍
- 简单介绍Java语言中内存管理的几个技巧
- 简单介绍java Enumeration
- [转]Java的开源项目:简单介绍Log4J的使用
- linux下java和javac的简单介绍
- 简单介绍java Enumeration
- 【java并发】基于JUC CAS原理,自己实现简单独占锁
- Java线程简单使用介绍zz
- java关于线程简单介绍
- java反射简单介绍
- Java中的多线程简单介绍(带代码)
- Java 理论与实践:并发在一定程度上使一切变得简单
- java数组简单介绍以及其方法
- java中常用jar包简单介绍(持续更新)
- 简单介绍java Enumeration
- java开发工具简单介绍
- Java静态检测工具的简单介绍 - Sonar、Findbugs