ThreadLocal使用与原理

ThreadLocal用于创建线程局部变量,使用ThreadLocal创建的变量在每个线程中有独立的副本。什么叫线程局部变量?我用方法的局部变量做类比,在方法的第一行定义一个变量,则在方法代码块内的任何一行代码都能使用这个变量。类似的,使用ThreadLocal创建的线程局部变量,只要线程没终止,线程局部变量就存在。一个常见的用法:Servlet给每个http请求分配一个单独的线程处理,一个http请求对应一个线程,可以将request信息使用ThreadLocal存储,Service、Dao的方法不必都定义Request入参也能获取到request信息。

initialValue()方法

为了更好地讲解ThreadLocal的源码,我先讲解initialValue()方法,此方法的作用是返回当前线程的线程局部变量的初值。

class MyRandom{
    /**
     * ThreadLocal设置初始值。每次执行MyRandom.random都会执行initialValue()。这类似一种延迟加载的功能,在调用方法的时候才创建对象、返回对象
     */
    public static ThreadLocal<Integer> random = new ThreadLocal(){
        @Override
        protected Integer initialValue() {
            return new Random().nextInt(10);
        }
    };
}

class MyRandomTest{
    public static void main(String[] args) {
        Integer num = MyRandom.random.get();
        System.out.println(num);
    }
}

在initialValue()、MyRandom.random.get()代码处打断点,查看方法调用栈。

1、MyRandom.random.get()调用ThreadLocal的setInitialValue()方法。
2、setInitialValue()调用initialValue()方法。

ThreadLocal的常见用法

ThreadLocal常用的方法:

     set(T value)   设置线程局部变量值
     get()          获取线程局部变量值
     remove()       删除线程局部变量值

在Spring应用中可使用ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes()获取当前请求的request。这是ThreadLocal的一种经典用法,同一个线程的多个方法之间使用线程局部变量承载数据,下面是一段模仿使用ThreadLocal保存request,然后在Controller、Service中使用RequestContextHolder获取request信息的代码。

@Data
class Request{
    private String data;

    public Request(String data) {
        this.data = data;
    }
}
class RequestContextHolder {
    public static ThreadLocal<Request> holder = new ThreadLocal<>();
}

class Servlet {
    public void process(String data){
        Request request = new Request(data);
        // 线程局部变量设置值
        RequestContextHolder.holder.set(request);
        System.out.println("Servlet.request: "+request);
        new Controller().process();
    }
}
class Controller {
    public void process(){
        // 获取线程局部变量值
        Request request = RequestContextHolder.holder.get();
        System.out.println("Controller.request: "+request);
        new Service().process();
    }
}
class Service {
    public void process(){
        Request request = RequestContextHolder.holder.get();
        System.out.println("Service.request: "+request);

        // 最后要移除线程局部变量,避免内存溢出
        RequestContextHolder.holder.remove();
    }
}

class HolderTest {
    public static void main(String[] args) {
        new Servlet().process("http请求");
    }
}

要理解ThreadLocal的原理,要先搞清楚Thread、ThreadLocalMap、ThreadLocal这3个类的关系,这3个类的关系如下图所示

有一点是我初学ThreadLocal非常困惑的地方,使用ThreadLocal时,我们使用threadLocal.set(data)、threadLocal.get(),threadLocal.remove()方法,导致我先入为主地认为data是存储在ThreadLocal类中,这是非常错误的理解。正确的理解是上图中展示的关系:

1、Thread类有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals

// Thread类部分代码
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}    

2、ThreadLocal.ThreadLocalMap是ThreadLocal的静态内部类,用于维护线程本地值,使用哈希表(key-value结构)存储数据,key是ThreadLocal类型,value是数据。

public class ThreadLocal<T> {
	/**
	 * 静态内部类
	 */
	static class ThreadLocalMap {
		/**
		 * ThreadLocalMap使用Entry保存数据,Entry是一个key、value结构
		 * key是ThreadLocal类型,value是Object类型
		 */
	    static class Entry extends WeakReference<ThreadLocal<?>> {
	        Object value;
	        Entry(ThreadLocal<?> k, Object v) {
	            super(k);
	            value = v;
	        }
	    }

	    private Entry[] table;

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

3、执行threadLocal.set(data)时,data并不是存储在threadLocal对象中,而是存储在当前线程Thread类的threadLocals这个key-value结构的成员变量中,threadLocal实例作为key,对应的value是data。

知道了Thread,ThreadLocalMap、ThreadLocal3者的关系,分析ThreadLocal的set(T value)、get()、remove()方法就非常简单了。

ThreadLocal类的set(T value)源码分析

public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // getMap(Thread t)方法只有一行,return t.threadLocals; 返回线程类的threadLocals
    ThreadLocal.ThreadLocalMap map = getMap(t);
    if (map != null)
        // 以ThreadLocal作为key,数据作为value存储到threadLocals中
        map.set(this, value);
    else
        // 如果t.threadLocals是空,则使用带参构造函数创建map并保存数据
        // createMap(Thread t, T firstValue)源码也只有一行 t.threadLocals = new ThreadLocalMap(this, firstValue);
        createMap(t, value);
}

ThreadLocal类的get()源码分析

public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取线程的 t.threadLocals
    ThreadLocal.ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 获取t.threadLocals的Entry中threadLocal对应value
        ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果t.threadLocals不为空,又找不到value。则调用setInitialValue(),
    // setInitialValue()会调用initialValue(),文章开头已经介绍了initialValue()方法
    return setInitialValue();
}

从set(T value)、get()方法可以看出来,set(T value)方法的参数value是存储到了Thread的threadLocals中,ThreadLocal创建的对象RequestContextHolder.holder在用法上有点类似于一个工具类,并且RequestContextHolder.holder献祭了自己,把自己作为key和value关联起来。

ThreadLocal类的remove()源码分析

public void remove() {
    // 返回当前线程的成员变量threadLocals
    ThreadLocal.ThreadLocalMap m = getMap(Thread.currentThread());
    // 删除线程成员变量threadLocals中以ThreadLocal对象为key的键值对。
    if (m != null)
        m.remove(this);
}

线程局部变量使用完后,不要忘记执行删除操作,不然会有内存溢出风险。

内存溢出与弱引用

如果线程是使用new Thread(runnable)这种方式创建的,则线程终止后,线程中的threadLocals会被回收,自然不会发生内存溢出。但是如果线程是使用线程池创建的,并且一直存活,threadLocals会常驻内存,若存活线程中有很多ThreadLocal对象执行了set(value)方法,而不执行remove()方法,将导致存活线程的threadLocals越来越大。

以下是ThreadLocalMap.Entry的部分代码

static class ThreadLocalMap {
	/**
	 * Entry继承了弱引用,ThreadLocal类型的key是弱引用
	 */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        // Entry中的value仍然是强引用
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

ThreadLocal.ThreadLocalMap.Entry继承了弱引用。弱引用的特点:如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收。但只是key为弱引用,value还是强引用,若还有其他变量引用了value,会导致Entry无法被GC回收。java设计者考虑到了ThreadLocal会发生内存溢出的场景,并做了一些处理,比如set(T value),remove()方法会调用resize()方法,resize()方法判断Entry的key是否为null,若key为null,则将value设置为null,帮助JVM在GC时清理内存。

假设key为不被强引用关联,Entry的键值对被回收的过程如下:

虽然java设计者做了一些优化,以避免内存溢出。但如果Entry中的key被强引用关联,并且线程是线程池中一直存活的线程,仍然有内存溢出风险,为了保险起见,使用ThreadLocal仍然要手动调用remove()方法。

ThreadLocal与线程安全

ThreadLocal的官方定义:This class provides thread-local variables。我英文很菜,这句英文的大致意思是这个类可用于创建线程本地变量。我仍然使用方法局部变量做类比。

class VariableDemo {
    public static Map commonData = new HashMap();

    public void fn1(){
        // 线程安全
        Map a = new HashMap<>();
        
        // 非线程安全
        Map b = commonData;
    }
}

上述代码的a是线程安全的,b是非线程安全的(当然,变量b这种使用方式确实是脑残)。使用ThreadLocal也存在此的问题,即执行ThreadLocal的set(T value)方法,value本身必须线程安全的。以下是线程不安全代码演示

@Data
class Request{
    private String data;

    public Request(String data) {
        this.data = data;
    }
}
class RequestContextHolder {
    public static ThreadLocal<Request> holder = new ThreadLocal<>();
}

class Servlet {
    public void process(String data){
        // 线程局部变量的值是线程不安全的
        RequestContextHolder.holder.set(HolderTest.request);
        new Controller().process();
    }
}
class Controller {
    public void process(){
        // 获取线程局部变量值,并将request的data属性改成当前线程的名字
        Request request = RequestContextHolder.holder.get();
        request.setData(Thread.currentThread().getName());
        new Service().process();
    }
}
class Service {
    public void process(){
        // 睡眠一段时间
        HolderTest.sleep();
        Request request = RequestContextHolder.holder.get();
        System.out.println(Thread.currentThread().getName() + " 线程与request " + request);
        // 最后要移除线程局部变量,避免内存溢出
        RequestContextHolder.holder.remove();
    }
}

class HolderTest {
    // 线程不安全的变量
    public static Request request = new Request("");

    public static void sleep(){
        try {
            TimeUnit.MILLISECONDS.sleep(100 + new Random().nextInt(500));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> new Servlet().process("")).start();
        }
    }

}

这段代码与文章开头的代码最大的不同之处是RequestContextHolder.holder.set(XX);设置的值不同。

文章开头,线程安全的代码:

    public void process(String data){
        // request在方法内部创建,是线程安全的,类似于Map a = new HashMap<>(); 这种写法
        Request request = new Request(data);
        RequestContextHolder.holder.set(request);
    }

线程不安全的代码

    public void process(String data){
        // set方法的入参是线程不安全的,类似于Map b = commonData;这种写法
        RequestContextHolder.holder.set(HolderTest.request);
        new Controller().process();
    }

如果大家还是有点懵,请联系Thread、ThreadLocalMap、ThreadLocal这3个类的关系来理解,比方说有两个线程执行了RequestContextHolder.holder.set(HolderTest.request);这句代码,线程与数据的关系如下图

两个线程执行RequestContextHolder.holder.get();得到的是同一个对象,然后修改这个对象,当然会出现线程不安全的情况。

再啰嗦几句,如果要让上面的代码变成线程安全的,要怎么做呢?答案:给读取、修改HolderTest.request的代码加上锁即可。例如给new Servlet().process("");加上synchronized

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                synchronized (LOCK){
                    new Servlet().process("");
                }
            }).start();
        }
    }

这做法也类似将 Map b = commonData;变为线程安全的做法一样

class VariableDemo {
    public static Object LOCK = new Object();
    public static Map commonData = new HashMap();
    public void fn1(){
        // 线程安全
        Map a = new HashMap<>();
        
        // 加上锁
        synchronized (LOCK){
            Map b = commonData;
            b.put("xxx", Thread.currentThread().getName());
            b.get("xxx");
            b.put("aaa", "aa");
            b.remove("xxx");
        }
    }
}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值