基本概念和用法

线程本地变量是说,每个线程都有同一个变量的独有拷贝,这个概念听上去比较难以理解,我们先直接来看类TheadLocal的用法。

ThreadLocal是一个泛型类,接受一个类型参数T,它只有一个空的构造方法,有两个主要的public方法:

public T get()
public void set(T value)

set就是设置值,get就是获取值,如果没有值,返回null,看上去,ThreadLocal就是一个单一对象的容器,比如:

public static void main(String[] args) {
ThreadLocal<Integer> local = new ThreadLocal<>();
local.set(100);
System.out.println(local.get());
}

输出为100。

那ThreadLocal有什么特殊的呢?特殊发生在有多个线程的时候,看个例子:

public class ThreadLocalBasic {
static ThreadLocal<Integer> local = new ThreadLocal<>(); public static void main(String[] args) throws InterruptedException {
Thread child = new Thread() {
@Override
public void run() {
System.out.println("child thread initial: " + local.get());
local.set(200);
System.out.println("child thread final: " + local.get());
}
};
local.set(100);
child.start();
child.join();
System.out.println("main thread final: " + local.get());
}
}

local是一个静态变量,main方法创建了一个子线程child,main和child都访问了local,程序的输出为:

child thread initial: null
child thread final: 200
main thread final: 100

这说明,main线程对local变量的设置对child线程不起作用,child线程对local变量的改变也不会影响main线程,它们访问的虽然是同一个变量local,但每个线程都有自己的独立的值,这就是线程本地变量的含义。

除了get/set,ThreadLocal还有两个方法:

protected T initialValue()
public void remove()

initialValue用于提供初始值,它是一个受保护方法,可以通过匿名内部类的方式提供,当调用get方法时,如果之前没有设置过,会调用该方法获取初始值,默认实现是返回null。remove删掉当前线程对应的值,如果删掉后,再次调用get,会再调用initialValue获取初始值。看个简单的例子:

public class ThreadLocalInit {
static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){ @Override
protected Integer initialValue() {
return 100;
}
}; public static void main(String[] args) {
System.out.println(local.get());
local.set(200);
local.remove();
System.out.println(local.get());
}
}

输出值都是100。

使用场景

ThreadLocal有什么用呢?我们来看几个例子。

DateFormat/SimpleDateFormat

ThreadLocal是实现线程安全的一种方案,比如对于DateFormat/SimpleDateFormat,我们在32节介绍过日期和时间操作,提到它们是非线程安全的,实现安全的一种方式是使用锁,另一种方式是每次都创建一个新的对象,更好的方式就是使用ThreadLocal,每个线程使用自己的DateFormat,就不存在安全问题了,在线程的整个使用过程中,只需要创建一次,又避免了频繁创建的开销,示例代码如下:

public class ThreadLocalDateFormat {
static ThreadLocal<DateFormat> sdf = new ThreadLocal<DateFormat>() { @Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
}; public static String date2String(Date date) {
return sdf.get().format(date);
} public static Date string2Date(String str) throws ParseException {
return sdf.get().parse(str);
}
}

需要说明的是,ThreadLocal对象一般都定义为static,以便于引用。

ThreadLocalRandom

即使对象是线程安全的,使用ThreadLocal也可以减少竞争,比如,我们在34节介绍过Random类,Random是线程安全的,但如果并发访问竞争激烈的话,性能会下降,所以Java并发包提供了类ThreadLocalRandom,它是Random的子类,利用了ThreadLocal,它没有public的构造方法,通过静态方法current获取对象,比如:

public static void main(String[] args) {
ThreadLocalRandom rnd = ThreadLocalRandom.current();
System.out.println(rnd.nextInt());
}

current方法的实现为:

public static ThreadLocalRandom current() {
return localRandom.get();
}

localRandom就是一个ThreadLocal变量:

private static final ThreadLocal<ThreadLocalRandom> localRandom =
new ThreadLocal<ThreadLocalRandom>() {
protected ThreadLocalRandom initialValue() {
return new ThreadLocalRandom();
}
};
复制代码

上下文信息

ThreadLocal的典型用途是提供上下文信息,比如在一个Web服务器中,一个线程执行用户的请求,在执行过程中,很多代码都会访问一些共同的信息,比如请求信息、用户身份信息、数据库连接、当前事务等,它们是线程执行过程中的全局信息,如果作为参数在不同代码间传递,代码会很啰嗦,这时,使用ThreadLocal就很方便,所以它被用于各种框架如Spring中,我们看个简单的示例:

public class RequestContext {
public static class Request { //...
}; private static ThreadLocal<String> localUserId = new ThreadLocal<>();
private static ThreadLocal<Request> localRequest = new ThreadLocal<>(); public static String getCurrentUserId() {
return localUserId.get();
} public static void setCurrentUserId(String userId) {
localUserId.set(userId);
} public static Request getCurrentRequest() {
return localRequest.get();
} public static void setCurrentRequest(Request request) {
localRequest.set(request);
}
}

在首次获取到信息时,调用set方法如setCurrentRequest/setCurrentUserId进行设置,然后就可以在代码的任意其他地方调用get相关方法进行获取了。

基本实现原理

ThreadLocal是怎么实现的呢?为什么对同一个对象的get/set,每个线程都能有自己独立的值呢?我们直接来看代码。

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

它调用了getMap,getMap的代码为:

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

返回线程的实例变量threadLocals,它的初始值为null,在null时,set调用createMap初始化,代码为:

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

从以上代码可以看出,每个线程都有一个Map,类型为ThreadLocalMap,调用set实际上是在线程自己的Map里设置了一个条目,键为当前的ThreadLocal对象,值为value。ThreadLocalMap是一个内部类,它是专门用于ThreadLocal的,与一般的Map不同,它的键类型为WeakReference<ThreadLocal>,我们没有提过WeakReference,它与Java的垃圾回收机制有关,使用它,便于回收内存,具体我们就不探讨了。

get方法的代码为:

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}

通过线程访问到Map,以ThreadLocal对象为键从Map中获取到条目,取其value,如果Map中没有,调用setInitialValue,其代码为:

private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

initialValue()就是之前提到的提供初始值的方法,默认实现就是返回null。

remove方法的代码也很直接,如下所示:

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

简单总结下,每个线程都有一个Map,对于每个ThreadLocal对象,调用其get/set实际上就是以ThreadLocal对象为键读写当前线程的Map,这样,就实现了每个线程都有自己的独立拷贝的效果。

线程池与ThreadLocal

我们知道,线程池中的线程是会重用的,如果异步任务使用了ThreadLocal,会出现什么情况呢?可能是意想不到的,我们看个简单的示例:

public class ThreadPoolProblem {
static ThreadLocal<AtomicInteger> sequencer = new ThreadLocal<AtomicInteger>() { @Override
protected AtomicInteger initialValue() {
return new AtomicInteger(0);
}
}; static class Task implements Runnable { @Override
public void run() {
AtomicInteger s = sequencer.get();
int initial = s.getAndIncrement();
// 期望初始为0
System.out.println(initial);
}
} public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new Task());
executor.execute(new Task());
executor.execute(new Task());
executor.shutdown();
}
}

对于异步任务Task而言,它期望的初始值应该总是0,但运行程序,结果却为:

0
0
1

第三次执行异步任务,结果就不对了,为什么呢?因为线程池中的线程在执行完一个任务,执行下一个任务时,其中的ThreadLocal对象并不会被清空,修改后的值带到了下一个异步任务。那怎么办呢?有几种思路:

  1. 第一次使用ThreadLocal对象时,总是先调用set设置初始值,或者如果ThreaLocal重写了initialValue方法,先调用remove
  2. 使用完ThreadLocal对象后,总是调用其remove方法
  3. 使用自定义的线程池

我们分别来看下,对于第一种,在Task的run方法开始处,添加set或remove代码,如下所示:

static class Task implements Runnable {

    @Override
public void run() {
sequencer.set(new AtomicInteger(0));
//或者 sequencer.remove(); AtomicInteger s = sequencer.get();
//...
}
}

对于第二种,将Task的run方法包裹在try/finally中,并在finally语句中调用remove,如下所示:

static class Task implements Runnable {

    @Override
public void run() {
try{
AtomicInteger s = sequencer.get();
int initial = s.getAndIncrement();
// 期望初始为0
System.out.println(initial);
}finally{
sequencer.remove();
}
}
}

以上两种方法都比较麻烦,需要更改所有异步任务的代码,另一种方法是扩展线程池ThreadPoolExecutor,它有一个可以扩展的方法:

protected void beforeExecute(Thread t, Runnable r) { }

在线程池将任务r交给线程t执行之前,会在线程t中先执行beforeExecure,可以在这个方法中重新初始化ThreadLocal。如果知道所有需要初始化的ThreadLocal变量,可以显式初始化,如果不知道,也可以通过反射,重置所有ThreadLocal,反射的细节我们会在后续章节进一步介绍。

我们创建一个自定义的线程池MyThreadPool,示例代码如下:

static class MyThreadPool extends ThreadPoolExecutor {
public MyThreadPool(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
} @Override
protected void beforeExecute(Thread t, Runnable r) {
try {
//使用反射清空所有ThreadLocal
Field f = t.getClass().getDeclaredField("threadLocals");
f.setAccessible(true);
f.set(t, null);
} catch (Exception e) {
e.printStackTrace();
}
super.beforeExecute(t, r);
}
}

这里,使用反射,找到线程中存储ThreadLocal对象的Map变量threadLocals,重置为null。使用MyThreadPool的示例代码如下:

public static void main(String[] args) {
ExecutorService executor = new MyThreadPool(2, 2, 0,
TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>());
executor.execute(new Task());
executor.execute(new Task());
executor.execute(new Task());
executor.shutdown();
}

使用以上介绍的任意一种解决方案,结果就符合期望了。

小结

本节介绍了ThreadLocal的基本概念、用法用途、实现原理、以及和线程池结合使用时的注意事项,简单总结来说:

  • ThreadLocal使得每个线程对同一个变量有自己的独立拷贝,是实现线程安全、减少竞争的一种方案。
  • ThreadLocal经常用于存储上下文信息,避免在不同代码间来回传递,简化代码。
  • 每个线程都有一个Map,调用ThreadLocal对象的get/set实际就是以ThreadLocal对象为键读写当前线程的该Map。
  • 在线程池中使用ThreadLocal,需要注意,确保初始值是符合期望的。

Java之线程本地变量ThreadLocal-copy的更多相关文章

  1. 线程本地变量ThreadLocal源码解读

      一.ThreadLocal基础知识 原始线程现状: 按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步.但是Spring中的各种模板 ...

  2. 线程本地变量ThreadLocal (耗时工具)

    线程本地变量类 package king; import java.util.ArrayList; import java.util.List; import java.util.Map; impor ...

  3. 线程本地变量ThreadLocal

    一.本地线程变量使用场景 并发应用的一个关键地方就是共享数据.如果你创建一个类对象,实现Runnable接口,然后多个Thread对象使用同样的Runnable对象,全部的线程都共享同样的属性.这意味 ...

  4. 线程本地变量ThreadLocal (耗时工具)【原】

    线程本地变量类 package king; import java.util.ArrayList; import java.util.List; import java.util.Map; impor ...

  5. Java并发(二十):线程本地变量ThreadLocal

    ThreadLocal是一个本地线程副本变量工具类. 主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不同的 ...

  6. JAVA线程本地变量ThreadLocal和私有变量的区别

    ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些. 所以,在Java中编写线程局部变量的代码相对来说要笨 ...

  7. 深入理解线程本地变量ThreadLocal

    ThreadLocal理解: 假设在多线程并发环境中.一个可变对象涉及到共享与竞争,那么该可变对象就一定会涉及到线程间同步操作,这是多线程并发问题. 否则该可变对象将作为线程私有对象,可通过Threa ...

  8. 深入理解java:2.4. 线程本地变量 java.lang.ThreadLocal类

    ThreadLocal,很多人都叫它做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多. 可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那样每个线程可以访问自己内 ...

  9. Java并发机制(4)--ThreadLocal线程本地变量(转)

    个人理解: 说明:看了博客园中大神写的ThreadLocal的详解,感觉还是有些迷糊,下面用自己的理解简单描述下ThreadLocal的机制(难免有误): 1.首先ThreadLocal用于存储对应线 ...

  10. java线程本地变量

      ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量).也许把它命名为Thre ...

随机推荐

  1. 5.6 Linux Vim撤销和恢复撤销快捷键

    使用 Vim 编辑文件内容时,经常会有如下 2 种需求: 对文件内容做了修改之后,却发现整个修改过程是错误或者没有必要的,想将文件恢复到修改之前的样子. 将文件内容恢复之后,经过仔细考虑,又感觉还是刚 ...

  2. 4-1 C++运算符基本概念

    目录 4.1.1 基本概念 函数观点 左值和右值 运算符重载 4.1.2 优先级.结合律与求值顺序 优先级和结合律 求值顺序 书中表述 实践表明(猜想) 实践验证 可能的解释:编译器的优化行为 一些运 ...

  3. games101_Homework6

    实现 Ray-Bounding Volume 求交与 BVH 查找 在本次编程练习中,你需要实现以下函数: • IntersectP(const Ray& ray, const Vector3 ...

  4. 管理 Python 环境和依赖关系的工具 venv、virtualenv、pipenv 、poetry 、 miniforge 和 anaconda 的区别

    管理 Python 环境和依赖关系的工具 venv.virtualenv.pipenv .poetry . miniforge 和 anaconda 的区别 venv.virtualenv.pipen ...

  5. 使用ob_tools包收集分析oceanbase数据库oracle租户缓慢sql语句

    概述 1.手册目的: 本手册旨在提供一种系统化的方法论,以便发现和分析慢SQL语句.通过使用ob_tools包,收集和分析在交付期间,应用程序在不同场景下进行压测时所产生的慢SQL语句,从而实现性能调 ...

  6. pycharm生成的allure测试报告如何查看本地的index.html文件?

    pycharm生成的allure测试报告应该是通过服务启动查看,但是如果把这个文件保存到本地查看,直接打开页面无内容 可以使用allure-combine工具实现本地正常打开 `from allure ...

  7. dp题单vjudge 8.17

    HDU-1024 Max Sum Plus Plus https://acm.hdu.edu.cn/showproblem.php?pid=1024 可以想到用dp过,但是无论时间和空间都不够,然后就 ...

  8. dotnet学习笔记-专题06-过滤器和中间件-01

    1. 基本概念 在ASP.NET Core中,中间件和过滤器都是处理HTTP请求的重要组件,但它们在应用中的位置.作用范围以及使用方式有所不同. 1.1 中间件和过滤器的区别 1.1.1 中间件 位置 ...

  9. 整合Sleuth

    Sleuth是 springcloud 分布式跟踪解决方案. Sleuth 术语: 跨度(span ) :Sleuth 的基本工作单元,他用一个64位的id唯一标识.出ID外,span还包含 其他的数 ...

  10. java 去重元素,元素是一组没有顺序的字符

    1.需求描述: 有一个大集合,大集合中的元素是是一个小集合,要求在大集合中的小集合不能重复,小集合中的元素没有顺序. 例如有个大集合 [[a,b],[b,c]] 向这个元素中添加元素[b,a]就是添加 ...