ThreadLocal的使用及实现原理

开发 前端
ThreadLocal可以用来把实例变量共享成全局变量,让程序中所有的方法都可以访问到该变量。

前言

ThreadLocal直译是本地线程,但实际上它的译名是线程局部变量(ThreadLocalVariable)。ThreadLocal诞生的目的是隔离不同线程所使用的变量,官方对它的解释是:

提供了线程局部变量,是独立于变量的初始化副本”,也就是说它可以实现将某一个变量隔离在某个线程内,其它的线程无法访问和使用这个变量。

我们先来做一个测试,先不使用ThreadLocal,创建三个线程:

public class ThreadLocalTest {
public static int num = 0;
public static int numAdd() {
return num++;
}
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread(new MyRunnable());
t1.start();
t2.start();
t3.start();
}
public static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "-" + ThreadLocalTest.numAdd());
}
}

}
}

执行后发现控制台输出的是:

可以发现线程执行了numAdd()方法,从0-8跑了九次,num从0加到8,也就是说线程之间共享了静态变量,从而导致线程的不安全问题。

然后我们再使用ThreadLocal来进行测试。

public class ThreadLocalTest {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static int numAdd() {
threadLocal.set(threadLocal.get()+1);
return threadLocal.get();
}
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread(new MyRunnable());
t1.start();
t2.start();
t3.start();
}
public static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "-" + numAdd());
}
}

}
}

这里的numAdd方法使用了ThreadLocal的get()方法,这个方法调用了initialValue()方法并设置了返回值为0,通过调用这个方法+1,达到了num++的效果,这时候再看输出的结果。

可以看到,三个不同的线程间相互隔离,变量的取值互不相干,也就是说ThreadLocal使用了不相干的变量,或者说ThreadLocal为每一个线程准备了一个变量副本,那么它是如何实现的呢,我们点进ThreadLocal的源码看看。

这就是ThreadLocal的构成了,主要操作是get()和set()方法:

get() : 返回当前ThreadLocal的值

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

set() : 将当前线程对象的值存入ThreadLocalMap中

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

首先创建了当前Thread的对象,然后存入ThreadLocalMap中,对map进行判断,不为空就将this(当前Threadlocal对象)存入作为key,并获取对应的值,最后是调用了一个setInitialValue()方法去获得初始化的值。

ThreadLocalMap

介绍上面两个方法主要是是为了引出ThreadLocal的实现原理,即ThreadLocalMap的创建和使用。

官方注释中解释道,ThreadLocalMap是一个定制的哈希映射,只适用于维护ThreadLocal的值。在ThreadLocal类之外没有导出操作。类是包私有的,以允许在类线程中声明字段。为了帮助处理非常大且长期使用的用法,哈希表条目对键使用弱引用。

但是,由于不使用引用队列,只有当表开始耗尽空间时,才开始删除陈旧的条目。

点开ThreadLocalMap,可以看到一开始ThreadLocalMap定义了一个用于存储数据的Entry 类。

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

这个Entry类继承了弱引用类,众所周知Java有四种引用类型,其中弱引用就是每次JVM进行垃圾回收时,都会回收该对象,保证了ThreadLocal每次拷贝当前线程的值的时候所占的空间能被重新使用。

由get()方法可以得知,ThreadLocalMap的键(key)是ThreadLocal类的实例对象,value为用户的值。

那么ThreadLocalMap的引用是在哪里呢,在上面的set()方法里,调用了getMap()和createMap()方法。

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

可以看到这边调用了一个叫threadLocals的属性,点击这个属性发现跳到了Thread类中。

  /* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

所以这个属性便是ThreadLocalMap的引用了,那么ThreadLocal的实现原理也就很清晰了:

  1. 定义了一个ThreadLocalMap内部类,使用的是Map的键值对方式来存取数据,key是ThreadLocal类的实例对象,value为传值。
  2. 创建新的ThreadLocal对象,调用set()或get()方法时,也就是调用了ThreadLocalMap来进行操作。
  3. 使用ThreadLocal时,线程所使用的变量是独享的(私有的变量副本),其他线程无法访问,在使用过后(线程结束),这些变量会被GC回收。

使用ThreadLocal的原因

ThreadLocal可以用来把实例变量共享成全局变量,让程序中所有的方法都可以访问到该变量。

由于存到ThreadLocal的变量都是当前线程本身,其他线程无法访问,存到ThreadLocal中只是为了方便在程序中同一个线程之间传递这个变量(和解决线程安全没有关系)。

责任编辑:姜华 来源: 今日头条
相关推荐

2021-05-06 08:55:24

ThreadLocal多线程多线程并发安全

2023-09-08 08:20:46

ThreadLoca多线程工具

2015-03-10 13:55:31

JavaScript预解析原理及实现

2009-03-27 10:10:13

c#远程启动远程管理

2015-09-09 08:45:49

JavaThreadLocal

2017-03-02 10:49:37

推荐算法原理实现

2017-02-06 19:26:15

iOSCFArray开源

2023-02-28 11:27:50

线程处理解决共享变量

2021-06-10 08:29:15

Rollup工具前端

2023-12-18 09:39:13

PreactHooks状态管理

2015-12-02 15:29:32

HTTP网络协议代理原理

2015-12-02 14:10:56

HTTP网络协议代理原理

2023-08-31 08:12:23

应用场景业务异常HTTP

2019-11-12 11:15:39

setTimeout前端代码

2018-07-27 08:39:44

负载均衡算法实现

2010-05-12 10:53:04

Symbian开发

2019-09-30 08:28:53

Delta LakeSpark数据原理

2017-07-04 12:26:14

ARARKit

2009-09-22 17:13:53

Hibernate O

2013-05-17 15:34:45

点赞
收藏

51CTO技术栈公众号