作者:汤圆

个人博客: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. 使用场景:面试等场合

参考内容:

后记

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

Java并发:ThreadLocal的简单介绍的更多相关文章

  1. Java并发性和多线程介绍

    java并发性和多线程介绍: 单个程序内运行多个线程,多任务并发运行 多线程优点: 高效运行,多组件并行.读->操作->写: 程序设计的简单性,遇到多问题,多开线程就好: 快速响应,异步式 ...

  2. Java 并发和多线程(一) Java并发性和多线程介绍[转]

    作者:Jakob Jenkov 译者:Simon-SZ  校对:方腾飞 http://tutorials.jenkov.com/java-concurrency/index.html 在过去单CPU时 ...

  3. ThreadLocal的简单介绍

    ThreadLocal是什么 早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.使用这个工具类可以很简洁地 ...

  4. java反射机制的简单介绍

    参考博客: https://blog.csdn.net/mlc1218559742/article/details/52754310 先给出反射机制中常用的几个方法: Class.forName (& ...

  5. Java泛型使用的简单介绍

    目录 一. 泛型是什么 二. 使用泛型有什么好处 三. 泛型类 四. 泛型接口 五. 泛型方法 六. 限定类型变量 七. 泛型通配符 7.1 上界通配符 7.2 下界通配符 7.3 无限定通配符 八. ...

  6. Java Linked集合的简单介绍和常用方法的使用

    LinkedList的简单介绍 java.util.LinkedList 集合数据存储的结构是链表结构.LinkedList是一个双向链表在实际开发中,对一个集合元素的添加和删除,经常涉及到首尾操作, ...

  7. Java中NIO的简单介绍

    NIO基本介绍 Java NIO(New IO) 也有人称之为Java non-blocking IO 是从Java1.4版本开始引入的一个新的IO API,可以代替标准的IO API.NIO与原来的 ...

  8. java中数据流的简单介绍

    java中的I/O操作主要是基于数据流进行操作的,数据流表示了字符或者字节的流动序列. java.io是数据流操作的主要软件包 java.nio是对块传输进行的支持 数据流基本概念 “流是磁盘或其它外 ...

  9. java并发:CopyOnWriteArrayList简单理解

    Java集合的快速失败机制 “fail-fast” "fail-fast"是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fas ...

随机推荐

  1. Apache配置虚拟目录+Zend Studio访问

    1 概述 Apache配置虚拟目录,然后可以通过Zend Studio的工程去访问,只需要修改Apache的httpd.conf文件. 2 修改httpd.conf 找到Apache安装目录下的htt ...

  2. linux下Mysql 8.0.19 编译安装

    1 前言 linux下安装MySQL的方式有很多种,包括以仓库的方式安装(yum,apt,zypper),以包的方式安装(rpm,deb),以docker方式安装,从压缩包解压安装,从源码编译安装,这 ...

  3. java面试一日一题:mysql中的自增主键

    问题:请讲下mysql中的自增主键 分析:该问题主要考察对mysql中自增主键的掌握,使用场景及如何设置 回答要点: 主要从以下几点去考虑 1.什么自增主键 2.使用场景是什么: 3.innodb_a ...

  4. Bootstrap工具包--用于响应式布局和移动设备优先的web项目

    Bootstrap是用于前端开发的工具包,是一个css/html框架 用于响应式布局和移动设备优先的web项目 响应式布局--一个网站能兼容多个终端 有很多版本:v3,v4,v5   三个没啥区别  ...

  5. php自定义配置文件简单写法

    1 <?php 2 header("Content-type:text/html;charset=utf-8"); 3 4 $q = getconfig('rr'); 5 e ...

  6. 关于Number、parseInt、isNaN转化参数

    1.首先,关于NaN的相等判断 alert(NaN==NaN) //返回的是false: 2.isNaN 确定这个参数是否是数值或者是否可以被转化为数值:NaN是not a number 的缩写,所以 ...

  7. thinkphp5的extend怎么用?

    http://www.newthink.cc/2017/09/13/thinkphp5%E7%9A%84extend%E6%80%8E%E4%B9%88%E7%94%A8%EF%BC%9F/#i-2

  8. 「编程羽录」上线,程序员必备的这些技能你能get到嘛?

    大家好,我是小羽. 好久不见,给大家带来个好消息,小羽的全新专题「编程羽录」系列正式上新,主要是介绍一些关于面试题和经验总结的文章. 会为大家提供一些技术栈之外,程序员还需要的其他方面硬核知识,做到全 ...

  9. hdu4791水题

    题意:       打印东西,给你区间和每个区间的价格,然后输入任务张数,输出最少花费..    题解:       昨晚的小测试就有这个题目,当时蒙B了,怎么也读不懂题目,一直纠结怎么把150拆成1 ...

  10. hdu3715 二分+2sat+建图

    题意:       给你一个递归公式,每多一层就多一个限制,问你最多能递归多少层. 思路:      先分析每一层的限制 x[a[i]] + x[b[i]] != c[i],这里面x[] = 0,1, ...