ThreadLocal究竟是个啥?

一、关于

 ThreadLocal不是一个线程,而是一个线程的本地化对象,当某个变量在使用ThreadLocal维护时,它会在每个线程中创建独立的一个副本,不同线程中对这个变量的修改不会影响到其他线程中这个变量的值。
 ThreadLocal采用了空间换时间的思想,主要用来实现多线程环境下线程安全和保存线程上下文中的变量。
 可以笼统的理解它为线程范围下的全局变量存储对象。

二、API

  • public void set(T value) {}
    将T类型的值设置到ThreadLocal中
  • public T get() {}
    获取ThreadLocal对象的值
  • public void remove() {}
    移除ThreadLocal中的值

三、源码分析

1.1set方法

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
  1. 首先获取到当前线程。
  2. 根据getMap方法根据当前线程获取到ThreadLocalMap 。我们看一下getMap方法:
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

 直接返回的是t.threadLocals,threadLocals是Thread类的一个参数,类型是ThreadLocal中的内部类ThreadLocalMap,初始值为null。
在这里插入图片描述
 再看ThreadLocal类结构:

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;
            }
        }
        ...............
        private Entry[] table;
        ...............
}

 其内部维护了一个Entry数组,且注意Entry类是继承自WeakReference,弱引用对象,所以需要注意jvm回收引发的问题。

  1. 回到set方法
  • 如果获取出来的map不为空,就将当前ThreadLocal对象和value值放进去。
  • 如果map为空,就创建map
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

1.2get方法

    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();
    }
  1. 获取当前线程。
  2. 获取当前线程的所有ThreadLocal对象(ThradLocalMap)。
  3. 如果map不为空,就根据ThreadLocal对象获取到存储在其中的值。
  4. 如果map为空,则初始化将当前ThreadLocal对象中的值设为null,并放入ThreadLocalMap。

1.3remove方法

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

 获取当前线程的所有ThreadLocal对象,并将当前对象从ThreadLocalMap 中移除。
 这里需要着重注意,不然会引起jvm内存泄漏!
 上面说到了Entry是继承WeakReference的对象就是弱引用对象,而调用构造方法时,也说明了Entry的key是指向当前ThreadLocal的弱引用对象。
在这里插入图片描述
在这里插入图片描述
 弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
 如果Entry的key被回收了,那么其对应的值就不会被remove掉了。所以在使用完之后需要手动remove()。

四、使用示例

 这里我们来模拟将用户登录信息存放到ThreadLocal中,项目为SpringBoot项目,结构为:
在这里插入图片描述
 缓存工具类CacheUtil,此类用于提供对ThreadLocal增删查。

package com.yl.demo.util;

public class CacheUtil {
	
	private static ThreadLocal<String> store = new ThreadLocal<String>();

	public static String getStore() {
		return store.get();
	}

	public static void setStore(String value) {
		store.set(value);
	}
	
	public static void remove() {
		store.remove();
	}
}

 拦截器TestInterceptor,拦截所有请求,在进入控制器之前,在ThreadLocal中设置信息,在执行完控制器的方法后,移除ThreadLocal中的值。

package com.yl.demo.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import com.yl.demo.util.CacheUtil;

public class TestInterceptor implements HandlerInterceptor{
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		System.out.println(Thread.currentThread().getName());
		CacheUtil.setStore("这里面是用户登录信息");
		return true;
	}

	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
	}

	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
		CacheUtil.remove();
	}

}

 mvc配置类,让拦截器拦截所有路径。

package com.yl.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.yl.demo.interceptor.TestInterceptor;

@Configuration
public class MvcConfig implements WebMvcConfigurer {
	
	@Bean
	public TestInterceptor testInterceptor() {
		return new TestInterceptor();
	}

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(testInterceptor()).addPathPatterns("/**");
	}

}

 测试Controller

package com.yl.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.yl.demo.util.CacheUtil;

@RestController
public class TestController {

	@GetMapping("/get")
	public String doGet() {
		return Thread.currentThread().getName()+"-get : " + CacheUtil.getStore();
	}
}

 运行项目,在浏览器测试:
在这里插入图片描述
在这里插入图片描述
 可以看到,同一线程下都可以获取到ThreadLocal中的值。

五、子线程获取父线程中ThreadLocal的值

我们这里改造一下接口:

package com.yl.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.yl.demo.util.CacheUtil;

@RestController
public class TestController {

	@GetMapping("/get")
	public String doGet() {
		// 新起一个线程获取值
		new Thread(()-> {
			System.out.println(Thread.currentThread().getName()+"-get : " + CacheUtil.getStore());
		}).start();
		return Thread.currentThread().getName()+"-get : " + CacheUtil.getStore();
	}
}

 new Thread(()-> {}).start()这种写法是java8中的写法,如果jdk低于1.8,就会报错,那么可以根据自己的实际情况来写,这里实现的就是新启动一个线程来获取存储在ThreadLocal中的值,看能否取到。
在这里插入图片描述
在这里插入图片描述
 我们可以看到,新线程为什么取到的是null呢?
 因为ThreadLocal是线程本地变量,设置值的时候是http-nio-8080-exec-1这个线程设置的,对其他线程没有影响,所以Thread-103-get线程是取不到值的。
 在这个方法中,线程Thread-103-get就是线程http-nio-8080-exec-1的子线程。
 那么我们如果想在子线程中获取父线程的ThreadLocal值,应该怎样去做呢?
 只需要改变一个地方,那就是将ThreadLocal替换成InheritableThreadLocal:
在这里插入图片描述
 重启项目,测试:
在这里插入图片描述
在这里插入图片描述
 子线程中可以获取到父线程中ThreadLocal的值了!

六、InheritableThreadLocal

在这里插入图片描述
 InheritableThreadLocal是ThreadLocal的子类。
 那么值究竟是从哪里获取出来的呢?
 首先看一下InheritableThreadLocal类中相对于ThreadLocal的改动:

  • 重写了getMap方法:
	ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
	}
  • 重写了createMap方法:
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }

 再看一下Thread的创建过程:
在这里插入图片描述
在这里插入图片描述
 最终init方法(省略版):

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
		.................
        Thread parent = currentThread();
       	.................
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
      	.................
    }

 此处的currentThread()线程就是上文中的http-nio-8080-exec-1(父)线程。
 此时inheritThreadLocals为true,且parent线程的inheritableThreadLocals 不为空的,所以就会将parent的inheritableThreadLocals赋给新创建的线程的inheritableThreadLocals ,也就是上文中的Thread-103-get(子)线程。所以子线程中就有了父线程中所有ThreadLocal的值。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值