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

前言

庆祝1024程序员节,携手学习ThreadLocal_第1张图片
马上就是1024程序员节了,在此小航先祝大家:

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

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

目录

  • 前言
  • 一、ThreadLocal简介
  • 二、ThreadLocal的简单使用
  • 三、ThreadLocal源码剖析
    • ThreadLocal的set()方法:
    • ThreadLocal的get()方法:
    • ThreadLocal的remove()方法:
  • 四、ThreadLocal场景实战

一、ThreadLocal简介

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

庆祝1024程序员节,携手学习ThreadLocal_第2张图片
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对象,从而避免了内存泄露。

庆祝1024程序员节,携手学习ThreadLocal_第3张图片

示例代码:

/**
 * @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项目:

庆祝1024程序员节,携手学习ThreadLocal_第4张图片
2.引入相关依赖:

<dependency>
            <groupId>org.springframework.sessiongroupId>
            <artifactId>spring-session-data-redisartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>

<dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.83version>
        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;
    }
}

测试登录:

庆祝1024程序员节,携手学习ThreadLocal_第5张图片
测试获取用户信息:

庆祝1024程序员节,携手学习ThreadLocal_第6张图片
大功告成!

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

你可能感兴趣的:(Java,学习,java,jvm)