庆祝1024程序员节,携手学习ThreadLocal

前言

在这里插入图片描述
马上就是1024程序员节了,在此小航先祝大家:

public static void main(String[] args) {
	System.out.println("节日快乐!");
}

😊你好,我是小航,一个正在变秃、变强的文艺倾年。
🔔本文讲解实战ThreadLocal,欢迎大家多多关注!
🔔每天进步一点点,一起卷起来叭!

一、ThreadLocal简介

ThreadLocal叫做线程本地变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

在这里插入图片描述
ThreadLocal和Synchonized都用于解决多线程并发访问,但是ThreadLocal与synchronized有本质的区别:

synchronized(锁)ThreadLocal
原理同步机制采用了时间换空间的方式,只提供一份变量,让不同线程排队访问(临界区排队)采用空间换时间的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不相干扰
侧重点多个线程之间访问资源的同步多线程中让每个线程之间的数据相互隔离

二、ThreadLocal的简单使用

示例代码:

/**
 * @author artboy
 */
public class ThreadLocalDemo {

    private static ThreadLocal<String> localVar = new ThreadLocal<String>();

    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar.get());
        //清除本地内存中的本地变量
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        localVar.remove();
    }
    public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {
            @Override
            public void run() {
                ThreadLocalDemo.localVar.set("local_A");
                print("A");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());

            }
        },"A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                ThreadLocalDemo.localVar.set("local_B");
                print("B");
                System.out.println("after remove : " + localVar.get());

            }
        },"B").start();
    }
}


A :local_A
B :local_B
after remove : null
after remove : null

从示例中我们可以看到:两个线程分表获取了自己线程存放的变量,他们之间变量的获取并不会错乱。

三、ThreadLocal源码剖析

ThreadLocal主要有以下几个方法:

public T get() { } // 用来获取ThreadLocal在当前线程中保存的变量副本
public void set(T value) { } //set()用来设置当前线程中变量的副本
public void remove() { } //remove()用来移除当前线程中变量的副本
protected T initialValue() { } //initialValue()是一个protected方法,一般是用来在使用时进行重写的

ThreadLocal的set()方法:

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
    	//1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
       		// 初始化thradLocalMap 并赋值
            createMap(t, value);
    }

从上面的代码可以看出,ThreadLocal set赋值的时候首先会获取当前线程thread,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap(获取thread线程中的ThreadLocalMap属性)。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。

那么ThreadLocalMap又是什么呢,还有createMap又是怎么做的,我们继续往下看。

public class ThreadLocal<T> {

  static class ThreadLocalMap {
        /**
         * The entries in this hash map extend WeakReference, using	
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }       
    }
}

可看出ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。

Java中的引用类型:

  • 强引用(Strong Reference):通常我们通过new来创建一个新对象时返回的引用就是一个强引用,若一个对象通过一系列强引用可到达,它就是强可达的(strongly reachable),那么它就不被回收
  • 弱引用(Weak Reference):弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存
  • 软引用(Soft Reference):软引用和弱引用的区别在于,若一个对象是弱引用可达,无论当前内存是否充足它都会被回收,而软引用可达的对象在内存不充足时才会被回收,因此软引用要比弱引用“强”一些
  • 虚引用(Phantom Reference):虚引用是Java中最弱的引用,那么它弱到什么程度呢?它是如此脆弱以至于我们通过虚引用甚至无法获取到被引用的对象,虚引用存在的唯一作用就是当它指向的对象被回收后,虚引用本身会被加入到引用队列中,用作记录它指向的对象已被回收。

判断弱引用对象的关键在于只具有弱引用的对象,也就是说,如果一个对象有强引用,那么在系统GC时,是不会回收此对象的,也不会释放弱引用。

这里为什么要使用弱引用呢?

原因是如果不使用弱引用,那么当持有value的强引用释放掉后,当线程没有回收释放时,threadLocalMap会一直持有ThreadLocal以及value的强应用,导致value不能够被回收,从而造成内存泄漏。

关于弱引用,如果有小伙伴不太懂,请康这个栗子:(弱引用回收测试)
WeakReferenceDemo.java

import java.lang.ref.WeakReference;

/**
 * 弱引用回收测试
 *
 * @author artboy
 */
public class WeakReferenceDemo {

    public static WeakReference<String> weakReference;

    public static void main(String[] args) {

        test();
        // 可以输出hello值,此时两个弱引用扔持有对象,而且未进行gc
        System.out.println("未进行gc时,只有弱引用指向value内存区域:" + weakReference.get());

        // 此时已无强一用执行"value"所在内存区域,gc时会回收弱引用
        System.gc();

        // 此时输出都为null
        System.out.println("进行gc时,只有弱引用指向value内存区域:" + weakReference.get());

    }

    public static void test() {
        String hello = new String("value");

        weakReference = new WeakReference<>(hello);

        System.gc();
        // 此时gc不会回收弱引用,因为字符串"value"仍然被hello对象强引用
        System.out.println("进行gc时,强引用与弱引用同时指向value内存区域:" + weakReference.get());

    }
}

进行gc时,强引用与弱引用同时指向value内存区域:value
未进行gc时,只有弱引用指向value内存区域:value
进行gc时,只有弱引用指向value内存区域:null

分析输出结果可以看出:

  • 当有强引用指向value内存区域时,即使进行gc,弱引用也不会被释放,对象不回被回收。

  • 当无强引用指向value内存区域是,此时进行gc,弱引用会被释放,对象将会执行回收流程。


通过使用弱引用,当ThreadLocal的强引用释放掉后,通过一次系统GC检查,发现ThreadLocal对象只有threadLocalMap中Entry的弱引用持有,此时根据弱引用的机制就会回收ThreadLocal对象,从而避免了内存泄露。

在这里插入图片描述

示例代码:

/**
 * @author artboy
 */
public class ThreadLocalDemo2 {

    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {

        ThreadLocalDemo2.threadLocal.set("hello world main");
        // main方法中ThreadLocal的变量threadLocal
        System.out.println(ThreadLocalDemo2.threadLocal.get());


        try {
            // 在new Thread()中对ThreadLocal的变量threadLocal进行修改
            Thread thread = new Thread() {
                @Override
                public void run() {
                    ThreadLocalDemo2.threadLocal.set("hello world thread");
                    System.out.println(ThreadLocalDemo2.threadLocal.get());
                };
            };
            thread.start();
            thread.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
        // main方法中ThreadLocal的变量threadLocal
        System.out.println(ThreadLocalDemo2.threadLocal.get());
        // 调用remove方法删除threadLocal副本,以防内存泄露
        threadLocal.remove();
    }
}

hello world main
hello world thread
hello world main

不难看出,我们在new Thread()中对ThreadLocal的变量threadLocal进行修改后,在main线程中再次输出,其值并没有收到影响,他们修改的分别是各自的副本,不会对其他副本有影响。

ThreadLocal的get()方法:

/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
public T get() {
	// 1、获取当前线程
    Thread t = Thread.currentThread();
    // 2、获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 3、如果map数据不为空,
    if (map != null) {
    	// 3.1、获取threalLocalMap中存储的值
    	// 注意这里获取键值对传进去的是 this,而不是当前线程t
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
    return setInitialValue();
}

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

//ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
}

//这个是threadlocal 的内部方法
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
}

首先是通过调用initialValue,initialValue是protected方法,初始化ThreadLocal时可以重写此函数,相当于延迟加载,然后通过getMap创建threadLocals,如果threadLocals不存在时,会调用createMap创建一个初始大小为16的Entry数组table,并新建一个Entry存入table中。这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)

我们顺着思路,再康康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类是集成自WeakReference,然后使用ThreadLocal作为了键,也就是说这里的ThreadLocal是一个弱引用在GC的时候会被回收。


public T get() {
	...
    // 3、如果map数据不为空,
    if (map != null) {
    	// 3.1、获取threalLocalMap中存储的值
    	// 注意这里获取键值对传进去的是 this,而不是当前线程t
        ThreadLocalMap.Entry e = map.getEntry(this);
    ...
}

如果map存在,则会调用map的getEntry方法,getEntry方法实现:

private Entry getEntry(ThreadLocal<?> key) {
    // 通过hash算出数组下标
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        // 如果取出Entry,并且e.get也就是referent与threadLocal相同,则说明是需要的值,返回Entry对象e ,判断e.get() = key 是解决hash碰撞的情况
        return e;
    else
        // 如果下标i的Entry不存在或者 其threadLocal不相同,则执行此
        return getEntryAfterMiss(key, i, e);
}


private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    
    while (e != null) {
        // 说明有此entry,可能是hash碰撞的结果
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            // 处理已无引用的ThreadLocal变量等,解决内存泄漏的机制之一
            expungeStaleEntry(i);
        else
            // 下标+1 
            i = nextIndex(i, len);
        e = tab[i];
    }
    // 如果getEntry中获取的entry=null,则说明无此ThreadLocal变量,返回null
    return null;
}

expungeStaleEntry 方法:

//删除可以释放的Entry
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            //如果发现ThreadLocal已经被释放掉,则通过这里来释放value的引用,以及删除数组table中的Entry
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                //重新设置Entry在table中的位置
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

通过对get方法的大致分析,可以分为几个阶段:

1)判断Map是否存在,如果不存在初始化Map以及table等

2)如果已存在,并且获取到Entry,则返回

3)如果不存在,则调用expungeStaleEntry清除需要释放的ThreadLocal、释放对value的一用,从table中删除相应下标的Entry,以及重新设置元素在table中的位置

ThreadLocal的remove()方法:

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

/**
 * ThreadeLocalMap
 * Remove the entry for key.
 */
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

remove方法相对简单,通过hashcode计算出下标,然后判断key与要删除的ThreadLocal是否一致,如果一致,释放掉相应的引用,并调用expungeStaleEntry方法清理其他的可以释放的对象。

四、ThreadLocal场景实战

场景:我们访问方法时常常需要获取用户信息,在并发场景下一个用户对应一个请求线程,因为拦截器与对应拦截方法都是属于同一个线程的,我们每次访问方法之前,通过拦截器获取到用户信息,根据当前线程就自然也就保存对应的用户信息,存取比较方便

示例Demo:

1.初始化Spring项目:

在这里插入图片描述
2.引入相关依赖:

<dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>

3.配置application.yml

spring:
  session:
    store-type: redis
  redis:
    # 地址
    host: .....
    # 端口
    port: 6379
    # 数据库索引(db0,db1,db2...不同业务可以放在不同数据库中)
    database: 0
    # 密码
    password: '....'

4.创建常量类:MyConstant

package com.example.thread_local_demo.constant;

/**
 * @author xh
 * @Date 2022/10/21
 */
public class MyConstant {
    public static final String TEMP_USER_COOKIE_NAME = "my_cookie";
    public static final int TEMP_USER_COOKIE_EXPIRE_TIME = 30;
}

5.配置SessionConfig

package com.example.thread_local_demo.config;

import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import com.example.thread_local_demo.constant.MyConstant;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;

/**
 * @author xh
 * @Date 2022/10/21
 */
@Configuration
@EnableRedisHttpSession
public class MySessionConfig {
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericFastJsonRedisSerializer();
    }

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName(MyConstant.TEMP_USER_COOKIE_NAME);
        return serializer;
    }
}

6.创建Entity:UserInfoTo

package com.example.thread_local_demo.entity;

import lombok.Data;

import java.io.Serializable;

/**
 * @author xh
 * @Date 2022/10/21
 */
@Data
public class UserInfoTo implements Serializable {
    private static final long serialVersionUID = 1L;
    private Long userId;
    private String userKey;
}

7.创建自定义拦截器:MyInterceptor

package com.example.thread_local_demo.interceptor;

import com.example.thread_local_demo.constant.MyConstant;
import com.example.thread_local_demo.entity.UserInfoTo;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.LinkedHashMap;
import java.util.UUID;

/**
 * @author xh
 * @Date 2022/10/21
 */
public class MyInterceptor implements HandlerInterceptor {
    public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        // 获取用户登录信息
        HttpSession session = request.getSession();
        LinkedHashMap loginUser = (LinkedHashMap) session.getAttribute("loginUser");

        UserInfoTo userInfoTo = new UserInfoTo();
        if (loginUser != null) {
            userInfoTo.setUserId(Long.parseLong(loginUser.get("id").toString()));
        }

        // 遍历cookie
        Cookie[] cookies = request.getCookies();
        if (cookies != null && cookies.length > 0) {
            for (int i = 0; i < cookies.length; i++) {
                // 判断是否是游客
                if (MyConstant.TEMP_USER_COOKIE_NAME.equals(cookies[i].getName())) {
                    userInfoTo.setUserKey(cookies[i].getValue());
                }
            }
        }

        threadLocal.set(userInfoTo);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

        UserInfoTo userInfoTo = threadLocal.get();

        if (userInfoTo.getUserKey() == null || "".equals(userInfoTo.getUserKey())) {
            userInfoTo.setUserKey(UUID.randomUUID().toString());
        }

        Cookie cookie = new Cookie(MyConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
        cookie.setMaxAge(MyConstant.TEMP_USER_COOKIE_EXPIRE_TIME);

        response.addCookie(cookie);
    }

}

8.将自定义拦截器注册进来:MyWebMvcConfig

package com.example.thread_local_demo.config;

import com.example.thread_local_demo.interceptor.MyInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author xh
 * @Date 2022/10/21
 */
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {

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

9.新建接口:

package com.example.thread_local_demo.controller;

import com.example.thread_local_demo.entity.UserInfoTo;
import com.example.thread_local_demo.entity.UserLoginVo;
import com.example.thread_local_demo.interceptor.MyInterceptor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * @author xh
 * @Date 2022/10/21
 */
@RestController
public class ApiController {
    @GetMapping("/test")
    public String test() {
        UserInfoTo userInfoTo = MyInterceptor.threadLocal.get();
        System.out.println(userInfoTo);
        return userInfoTo.toString();
    }
	@GetMapping("/login/{id}")
    public String login(@PathVariable String id, HttpServletRequest request) {
        HttpSession session = request.getSession();
        LinkedHashMap<String, Long> loginUser = new LinkedHashMap<>();
        loginUser.put("id", 1L);
        session.setAttribute("loginUser", loginUser);
        return "登录成功:ID=" + id;
    }
}

测试登录:

在这里插入图片描述
测试获取用户信息:

在这里插入图片描述
大功告成!

如果您还有任何代码疑惑,可以私信留言小航!
上述涉及到的代码:https://gitee.com/lovexh666/thread_local_demo
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值