介绍
我们都知道,ThreadLocal能够保证每个线程都会拥有一份单独的数据,现在在对数据进行操作时,只会影响本线程的数据,不会对其它线程的数据有所影响。就好比,普通共享变量就好比公共电话,只有一个。而ThreadLocal就好比手机,人手一个。
那么它是怎么实现每个线程都有一份数据的呢,并且使用时需要注意哪些事项呢,我们一起来看看源码是怎么写的。
代码跟踪和分析
下面是一个ThreadLocal的简单案例
package threadlocal;
import java.security.SecureRandom;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author lmq
* @version 1.0
* @datetime 2025/4/5 16:35
**/
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
User user = new User();
AtomicInteger nums = new AtomicInteger();
CountDownLatch countDownLatch = new CountDownLatch(20);
for (int i = 0; i < 20; i++) {
new Thread(() -> {
int total = 0;
try {
total = new SecureRandom().nextInt(20);
for (int j = 0; j < total; j++) {
user.numLocalAdd();
}
nums.addAndGet(user.numLocal.get());
System.err.println(Thread.currentThread().getName() + ":" +user.numLocal.get());
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
countDownLatch.countDown();
user.numLocal.remove();
}
},"t" + (i + 1)).start();
}
countDownLatch.await();
System.out.println(user.numLocal.get());
System.out.println(nums.get());
}
}
class User{
public Integer num;
// 这里建议使用静态变量。因为这里是使用线程区分的,每个线程一份变量,不是通过类实例区分的。避免了每次new一个类实例都开辟一部分内存
ThreadLocal<Integer> numLocal = ThreadLocal.withInitial(() -> 0);
public void numLocalAdd(){
numLocal.set(numLocal.get() + 1);
}
}
代码中,新建20个线程,然后每个线程都对自己的ThreadLocal数据进行随机次数的++。我们先跟着代码走,先看下Thread类的构造器,有没有初始化ThreadLocal数据的相关代码。
上面的代码,新建线程会先进入这个构造方法,第一个参数是传入的线程任务,第二个是线程名称。我们看下一步
这个方法的参数,第一个是线程组,第二个是线程任务,第三个是线程名称,第四个是栈大小。然后再看下一步
这个方法太长,我就把代码复制过来了。然后顺便增加了一些注释和解释。到这里,新建线程的方法就结束了,线程启动方法里面没什么内容,也和ThreadLocal无关,看来线程的ThreadLocal赋值和新建线程,启动线程无关。我们继续往下走
/**
* Initializes a Thread.
*
* @param g the Thread group
* @param target the object whose run() method gets called
* @param name the name of the new Thread
* @param stackSize the desired stack size for the new thread, or
* zero to indicate that this parameter is to be ignored.
* @param acc the AccessControlContext to inherit, or
* AccessController.getContext() if null
* @param inheritThreadLocals if {@code true}, inherit initial values for
* inheritable thread-locals from the constructing thread
*/
@SuppressWarnings("removal")
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// 线程名为空时便抛出一个空指针异常(如果是正常的操作新建线程,并且名称不传null的话,都会有线程名字的)
if (name == null) {
throw new NullPointerException("name cannot be null");
}
// 将线程名字设置到属性中去(private volatile String name;)它是一个私有的volatile变量
this.name = name;
// 获取当前线程(此方法是一个native方法,通过c++实现 public static native Thread currentThread();)
Thread parent = currentThread();
// 获取JVM的安全管理器实例(这里我没有启用,所以返回的是null)
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
// 若安全管理器不为null,则将新建线程的线程组设置为安全管理器实例的线程组
if (security != null) {
g = security.getThreadGroup();
}
/* If the security manager doesn't have a strong opinion
on the matter, use the parent thread group. */
// 若线程组为空,则将新建线程的线程组设置为父级线程(父级线程是创建线程的那个线程,我这里的案例,父级线程是main线程)
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
// 检查授权(里面是通过安全管理器检查,因为我没有开启,所以这一步是空代码)
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(
SecurityConstants.SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
// 线程组增加未启动线程
g.addUnstarted();
// 将新建线程的线程组设置为g(g通过刚才的一些代码,已经有值了)
this.group = g;
// 将当前线程的是否守护线程标识设置为父级线程的标识
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
/*
* inheritableThreadLocals属性用于继承父级线程的inheritableThreadLocals值
* InheritableThreadLocal类倒是确实可以将父线程的值传递给子线程
*/
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
this.tid = nextThreadID();
}
跟着这个断点走,我们来到了ThreadLocal的set和get方法。
set方法
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocal的ThreadLocalMap实例对象(其实就是获取Thread的threadLocals属性)
ThreadLocalMap map = getMap(t);
// 若线程是第一次操作ThreadLocal,map肯定是null(除非是线程池的线程,然后使用完又没有remove掉ThreadLocal的话就会有遗留)
if (map != null) {
map.set(this, value);
} else {
// 创建ThreadLocalMap,key是当前线程对象,value是外部传过来的值(因为key是Thread对象,所以每个线程都会有一份值,互不影响)
createMap(t, value);
}
}
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;
}
}
// 上面的方法我们就不看了,和get类似,我们直接看这个方法
return setInitialValue();
}
private T setInitialValue() {
// 这里会直接返回null(所以我们在使用ThreadLocal时,最好要重写默认的initialValue方法,要不然就直接返回null了)
T value = initialValue();
// 下面的方法就和set差不多了
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
protected T initialValue() {
return null;
}
我们再来看看Thread和ThreadLocal的属性和内部类相关的
public class Thread implements Runnable {
// 用于维护每个线程的ThreadLocal值
ThreadLocal.ThreadLocalMap threadLocals = null;
// 用于维护父级的InheritableThreadLocal值
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
其它代码...
}
public class ThreadLocal<T> {
// 这是ThreadLocal的静态内部类,我们从Thread类中可以看到,内容主要也是放在这里面的
static class ThreadLocalMap {
/*
* Entry,用过集合的应该都知道,他就是k,v键值对,这里的key用的就是ThreadLocal实例对象,而value是我们需要存放的值
* 此处还使用了弱引用来对Entry进行包装
* tips:弱引用的对象,在下一次gc时就会被回收
* 所以下一次gc之后,key就会变成null
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
其它代码...
}
其它代码...
}
初始化ThreadLocal值避免空指针异常,使用ThreadLocal.withInitial进行初始化
我们定义的接口函数最终会在initialValue中被调用,而initialValue是重写父类ThreadLocal的方法,然后在get方法中会被调用
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
// new一个SuppliedThreadLocal类,并将自定义的接口函数方法传入构造器
return new SuppliedThreadLocal<>(supplier);
}
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.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;
}
}
// 上面的方法我们就不看了,和get类似,我们直接看这个方法
return setInitialValue();
}
private T setInitialValue() {
// 这里会直接返回null(所以我们在使用ThreadLocal时,最好要重写默认的initialValue方法,要不然就直接返回null了)
T value = initialValue();
// 下面的方法就和set差不多了
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
protected T initialValue() {
return null;
}
总结
Thread和ThreadLocal的关键结构代码
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
// 其它代码...
}
public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
其它代码...
}
其它代码...
}
1、存储在哪儿:每个线程的ThreadLocal值是绑定存储在Thread的threadLocals属性中
2、何时绑定:在当前线程第一次使用ThreadLocal变量时绑定
3、如何做到每个线程一份数据:第一,数据是绑定在Thread实例的属性中。第二,存储的数据结构是k,v键值对,并且key是当前ThreadLocal对象(保证唯一)
下面是ThreadLocal的引用关系示意图(实线为强引用,虚线为弱引用)
当线程(即Thread对象)正常结束任务时(非线程池情况下),线程中的属性(ThreadLocalMap对象)会被回收,此时所有与线程关联的ThreadLocal键值对(包括 Entry 中的 key 和 value)都会被释放。这种情况,即使不调用remove方法,也不会内存泄漏。
在线程池环境下,Thread对象有可能不会回收(核心线程不会回收),当系统GC时,ThreadLocal对象将被回收,此时Entry对象中的key会变为null,将无法访问。而ThreadLocalMap和T对象都有其它对象强引用,不会被回收,此时就会造成内存泄漏。解决办法就是调用remove方法。
评论区