2.ThreadLocal的使用

ThreadLocal提供线程局部变量,确保每个线程拥有独立的副本,避免并发问题。本文介绍了ThreadLocal的用法、应用场景及内存泄漏预防,包括如何在多线程环境中安全使用非线程安全类如SimpleDateFormat,以及在拦截器中存储用户信息等。同时,分析了ThreadLocal的底层实现,强调了正确使用ThreadLocal以防止内存泄漏的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是ThreadLocal

ThreadLocal提供线程局部变量,每个线程都有自己独立初始化的变量副本,这些变量副本只与其所在的线程独享,不同的线程之间的变量副本互不干扰。ThreadLocal实例通常是希望将变量状态与当前线程相关联(例如,用户ID或事务ID)。
例如,下面的类生成每个线程本地的唯一标识符。 线程的ID在第一次调用ThreadId.get()时被分配,并在后续调用中保持不变。

ThreadLocal用法

  1. 用法一: 在ThreadLocal第一次get的时候使用initialValue()把对象初始化出来,对象的初始化时机完全由自己控制,不受外部因素的影响
package threadlocal;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author pyh
 * @date 2021/4/22 23:42
 */
public class ThreadId01 implements Runnable {
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // threadId变量初始化一个和当前线程相关联的变量副本
    private static final ThreadLocal<Integer> threadId =
            new ThreadLocal<Integer>() {
                @Override
                protected Integer initialValue() {
                    return nextId.getAndIncrement();
                }
            };


    // 只要是在同一个线程内,通过threadId 就能获取到这个线程的同一个副本
    public static int get() {
        return threadId.get();
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new ThreadId01());
        }
        executorService.shutdown();

    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ":" + ThreadId01.get());
    }
}

部分结果如下:
在这里插入图片描述
由此得知,不同线程之间的结果不一样,相同线程拿到的都是同一份副本。

  1. 用法二:ThreadLocal对象由外部调用set方法设置进去,对象的生成时机由外部因素控制
package threadlocal;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author pyh
 * @date 2021/4/23 00:29
 */
public class ThreadId02 implements Runnable {
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // threadId变量初始化一个和当前线程相关联的变量副本
    private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() ;

    // 由外部调用该方法之后,ThreadLocal才会有对象
    public static void set() {
        threadId.set(nextId.getAndIncrement());
    }

    // 只要是在同一个线程内,通过threadId 就能获取到这个线程的同一个副本
    public static int get() {
        // 只有每个线程第一次进来的时候才会调用,因为第一次进来get获取的数据是空的
        if (threadId.get() == null) {
            set();
        }
        return threadId.get();
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new ThreadId02());
        }
        executorService.shutdown();

    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ":" + ThreadId02.get());
    }
}

执行结果如下:
在这里插入图片描述
结论和用法一一样。

ThreadLocal的使用场景

  1. 每个线程需要一个独享的对象,通常是非线程安全工具类,如:SimpleDateFormat和Random。
package threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author pyh
 * @date 2021/4/24 22:05
 */
public class ThreadLocalNormalUsage03 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    // dateFormat为非线程安全的共享资源
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage03().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        return dateFormat.format(date);
    }

}

SimpleDateFormat为共享资源,在多线程并发资源竞争的情况下的情况下,即使每次调用format格式日期的参数不一样,由于其并非线程安全类,也会出现格式化日期出现相同的结果。结果如下:
在这里插入图片描述
对于此问题,可以使用ThreadLocal来保证每个线程都拥有一份SimpleDateFormat副本,保证了线程安全,当然也可以在date方法加锁保证其线程安全,但是加锁会相对ThreadLocal更加消耗性能。

package threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author pyh
 * @date 2021/4/24 22:19
 */
public class ThreadLocalNormalUsage05 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    // 利用i作为SimpleDateFormat格式化日期参数,每次调用格式化的时间都不同
                    String date = new ThreadLocalNormalUsage05().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 08:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }
}

class ThreadSafeFormatter {

    /**
     * ThreadLocal初始化用法1
     */
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    /**
     * ThreadLocal初始化用法2
     */
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
            .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

即使多次反复执行都不会出现格式化日期相同的结果,部分结果截图如下:
在这里插入图片描述

  1. 每个线程内需要保存全局变量,例如在拦截器中获取用户信息,可以让不同方法直接获取,避免参数传递的麻烦。
package threadlocal;

import org.omg.PortableInterceptor.Interceptor;

/**
 * 描述:     演示ThreadLocal用法2:避免传递参数的麻烦
 * @author pyh
 * @date 2021/4/24 22:28
 */
public class ThreadLocalNormalUsage06 {
    public static void main(String[] args) {
        new InterceptorService().process();
    }
}

/**
 * 模拟用户登录后,拦截器将用户信息保存到ThreadLocal
 */
class InterceptorService {
    public void process() {
        User user = new User("超哥");
        UserContextHolder.holder.set(user);
        new OrderService().process();
    }
}

/**
 * 模拟用户下单需要到当前登录人信息
 */
class OrderService {
    public void process() {
        User user = UserContextHolder.holder.get();
        ThreadSafeFormatter.dateFormatThreadLocal.get();
        System.out.println("OrderService拿到用户名:" + user.name);
        new PaymentService().process();
    }
}

/**
 * 模拟下单后调用付款服务时需要到当前登录人信息
 */
class PaymentService {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("PaymentService拿到用户名:" + user.name);
        UserContextHolder.holder.remove();
    }
}

class UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {
    String name;

    public User(String name) {
        this.name = name;
    }
}

由于User对象在大多数服务中经常要用到的对象信息,如果把他作为形参传递,那大多数方法形参都需要添加User对象形参。虽然把User定义为全局变量也能解决形参问题,但是需求是不同登录用户(即不同线程)拿到的对象应该是不一样的,相同用户(相同线程)拿到的对象要求一样的,全局变量就无法解决这个问题了。结果如下:
在这里插入图片描述

ThreadLocal作用

  1. 让需要用到的对象在线程间隔离
  2. 在任何方法中都可以轻松获取到对象。
  3. 不需要加锁,也能达到线程安全,提高了执行效率。
  4. 相比于每个线程都新创建一个对象,ThreadLocal可以更高效地利用内存,节省开销。
  5. 免去传参的麻烦,使得代码更优雅,耦合度更低。

ThreadLocal底层原理

  • Thread、ThreadLocal和ThreadLocalMap的关系如下:
    在这里插入图片描述
    每个线程(Thread)都有一个ThreadLocalMap成员属性,该属性本质是一个Map,key是ThreadLocal,value是调用ThreadLocal.initialValue()或ThreadLocal.set()设置的值。一个ThreadLocalMap可以存储过个ThreadLocal。
class Thread{
	/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
  • ThreadLocal.get()
class ThreadLocal{
	/**
     * get获取initialValue()或set()设置进去的值
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
    	// 获取当前线程
        Thread t = Thread.currentThread();
        /*
        	获取当前线程的ThreadLocalMap,key是ThreadLocal,
        	value是initialValue()返回的值
        */
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	// 获取当前ThreadLocal对象使用set()方法设置进去的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 如果e为null,表示没有调用当前ThreadLocal对象的set方法,则尝试从重写的initialValue()获取值
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 从重写的initialValue()获取值
        return setInitialValue();
    }
}
  • ThreadLocal.initialValue()
class ThreadLocal{
	/**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
    	// 获取子类重写方法返回的值,默认返回null
        T value = initialValue();
        // 获取当前线程
        Thread t = Thread.currentThread();
        /*
        	获取当前线程的ThreadLocalMap,key是ThreadLocal,
        	value是initialValue()返回的值
        */
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else {
        	// ThreadLocalMap具有延迟加载的效果,第一次使用的时候才创建
            createMap(t, value);
        }
        return value;
    }
}
  • ThreadLocal.set()
class ThreadLocal{
	/**
     * 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) {
    	// 获取当前线程
        Thread t = Thread.currentThread();
        /*
        	获取当前线程的ThreadLocalMap,key是ThreadLocal,
        	value是initialValue()返回的值
        */
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else {
        	// ThreadLocalMap具有延迟加载的效果,第一次使用的时候才创建
            createMap(t, value);
        }
    }
}

通过源码发现,setInitialValue()方法和set方法逻辑类似,只不过setInitialValue()方法设置到ThreadLocalMap的值是initialValue()方法的返回值,而set()方法设置进去的是则是通过形参设置进去的。

  • Thread.remove()
class ThreadLocal{
	/**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
     	// 获取当前线程的ThreadLocalMap
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
         	// 移除当前ThreadLocal存储的值,再次调用get()方法的时候获取该值就为null,如果没有调用set()方法之前就调用get()方法,则还会触发调用initialValue()方法
             m.remove(this);
         }
     }
}
  • 总结:
  1. 当线程第一次调用get()方法访问变量时,将调用initialValue(),除非线程先前调用了set()方法。通常每个线程至多只会调用一次initialValue()方法,但如果已经调用了remove()方法,再调用get()方法,可以再次触发调用initialValue()方法。
  2. ThreadLocalMap是最终存储数据的存储结构,我们每次使用ThreadLocal的时候总会先new一个ThreadLocal对象,本质就是new一个ThreadLocalMap的键,然后通过该键获取相应的值。

ThreadLocal注意事项

  1. 内存泄漏
    即某个对象不再使用,但是占用的内存却不能回收。
  • 导致内存泄漏的原因:
class ThreadLocal{
	static class ThreadLocalMap {
		// 继承了弱引用父类
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
            	// key为弱引用
                super(k);
                // value为强引用
                value = v;
            }
        }
}

由前文可知,最终存储数据的是ThreadLocalMap,key是ThreadLocal,value是使用设置进去的值,而key最终保存在Entry的父类WeakReference(super(k)),所以key是以弱引用的方式存在,而value则是保存在Entry的成员属性中(value = v),所以value是以强引用的方式存在。

弱引用的特点是,如果这个对象只被弱引用关联(没有任何强关联),那么这个对象就可以被GC回收。

正常情况下,当线程终止,保存在ThreadLocalMap.Entry的value就会被GC回收,因为没有任何强引用了。

线程池设置初衷之一就是为了避免频繁创建销毁线程,如果使用了线程池,线程始终不终止,虽然ThreadLocalMap.Entry的key为弱引用可以被回收,但是ThreadLocalMap.Entry的value为强引用就不会被回收,如果不能被回收的对象存在很多,那么久可能引发OOM异常。

其实JDK设计者在ThreadLocalMap.resize()方法中也对相应问题做了处理,即如果弱引用key被回收了,则将value置为null,则value在相应的堆内存就可以被回收了。resize()是私有方法,该方法法作用是扩容,触发resize()方法调用的只有通过ThreadLocalMap.set(),并且并不是每次调用set()都会触发resize()方法,所以这并不不能作为解决内存泄漏的办法。在这里插入图片描述

  • 解决内存泄漏办法
    阿里规约中解决ThreadLocal内存泄漏的办法就是,在使用完ThreadLocal里的对象后,手动调用ThreadLocal.remove()方法。
  1. 空指针异常
public class ThreadLocalNPE {

    ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>();

    public void set() {
        longThreadLocal.set(Thread.currentThread().getId());
    }

    public Long get() {
        return longThreadLocal.get();
    }

    public static void main(String[] args) {
        ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(threadLocalNPE.get());
            }
        });
        thread1.start();
    }
}

正常情况,调用get()之前如果没有重写initialValue()方法或者调用set()方法,会返回一个null,不至于报空指针异常。但是如果将以上代码的get()改写成如下:

public long get() {
        return longThreadLocal.get();
    }

则会报空指针异常,这是因为基本类型在自动拆箱的时候所导致的。

  1. 共享对象
    如果每个线程中ThreadLocal.set()进去的对象本来就是一个共享对象,比如static对象,那么多线程情况下调用ThreadLocal.get()得到的还是一个这个共享对象本身,还是由并发访问问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NPException.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值