☰
ThreadLocal系列(一):原理与应用
2026/10/7 8:19:11 网站建设 项目流程

ThreadLocal

ThreadLocal介绍

ThreadLocal提供线程局部变量,每个线程独立有自己的变量副本,线程之间互不干扰(自己用自己的,别的线程拿不到)

基本使用
public class Basic { public static void main(String[] args) { ThreadLocal<String> threadLocal = new ThreadLocal<>(); threadLocal.set("hello"); System.out.println(threadLocal.get()); threadLocal.remove(); System.out.println(threadLocal.get()); } }

运行结果

hello null Process finished with exit code 0

把hello设置到ThreadLocal中,通过get方法可以获得,为什么?

内部原理

每个Thread对象内部都有一个ThreadLocalMap,set,get是操作的这个map

进入到Thread类中,可以看到下面内容

public class Thread implements Runnable { //省略。。。 /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; //省略。。。 }

再进入到ThreadLocal类中,可以看到下面内容

public class ThreadLocal<T> { //省略。。。 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 static final int INITIAL_CAPACITY = 16; private Entry[] table; private int size = 0; private int threshold; // Default to 0 private void setThreshold(int len) { threshold = len * 2 / 3; } //省略。。。 } //省略。。。 }

再来看Basic代码,**threadLocal.set("hello")**实际上就是把hello作为value存到了这个map中,而这个map的key是threadLocal变量

与HashMap的不同

Hash冲突

HashMap中发生Hash冲突的时候是用链表挂起来的,而ThreadLocalMap用的是开放定址。
当发生冲突的时候,会根据冲突的索引,查找下一个空位置,如果下一个位置也发生冲突,继续查找下一个位置,直到找到一个位置,把冲突的元素挂在这个位置上。

扩容

HashMap的扩容因子是0.75,而ThreadLocalMap的扩容因子是2/3

应用
上下文传递

多线程执行任务的时候,每个线程拿自己的用户信息,互不干扰

public class UserContext { private static final ThreadLocal<String> USER = new ThreadLocal<>(); public static void set(String username){ USER.set(username); } public static String get(){ return USER.get(); } public static void remove(){ System.out.println("删除了用户" + get()); USER.remove(); } }
public class OrderService { public void createOrder(){ String username = UserContext.get(); String threadName = Thread.currentThread().getName(); System.out.println("用户" + username + "创建了订单,当前线程为" + threadName); } }
public class UserContextTest { public static void main(String[] args) { String[] usernames = {"张三", "李四", "王五"}; //每个线程设置用户并调用创建订单方法 for (String username : usernames) { new Thread(()->{ try { UserContext.set(username); System.out.println("当前线程" + Thread.currentThread().getName() + "有一个用户" + username + "要调用创建订单方法"); new OrderService().createOrder(); } finally { UserContext.remove(); } }, "thread-" +username).start(); } } }

运行结果

当前线程thread-张三有一个用户张三要调用创建订单方法 当前线程thread-李四有一个用户李四要调用创建订单方法 当前线程thread-王五有一个用户王五要调用创建订单方法 用户张三创建了订单,当前线程为thread-张三 用户李四创建了订单,当前线程为thread-李四 删除了用户李四 用户王五创建了订单,当前线程为thread-王五 删除了用户张三 删除了用户王五
线程安全化

对于线程不安全的变量,防止出现数据覆盖问题,除了用锁等方案,还可以用ThreadLocal解决,每个线程内部维护自己的变量,防止其他线程修改覆盖
SimpleDateFormat线程安全化

如果运行下面代码就会发生覆盖情况

public class DateUtilUnsafe { //多个线程共享同一个SimpleDateFormat private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static String format(Date date){ return sdf.format(date); } public static void main(String[] args) throws InterruptedException { int count = 10; ExecutorService threadPool = Executors.newFixedThreadPool(count); CountDownLatch latch = new CountDownLatch(count); for (int i = 0; i < count; i++) { final long timeMillis = System.currentTimeMillis() + i * 1000L; threadPool.submit(()->{ String format = format(new Date(timeMillis)); System.out.println(Thread.currentThread().getName() + "格式化时间:" + format); latch.countDown(); }); } latch.await(); threadPool.shutdown(); } }

运行结果

pool-1-thread-1格式化时间:2026-10-06 10:49:10 pool-1-thread-6格式化时间:2026-10-06 10:49:14 pool-1-thread-3格式化时间:2026-10-06 10:49:13 pool-1-thread-7格式化时间:2026-10-06 10:49:15 pool-1-thread-4格式化时间:2026-10-06 10:49:13 pool-1-thread-9格式化时间:2026-10-06 10:49:16 pool-1-thread-5格式化时间:2026-10-06 10:49:13 pool-1-thread-10格式化时间:2026-10-06 10:49:18 pool-1-thread-2格式化时间:2026-10-06 10:49:10 pool-1-thread-8格式化时间:2026-10-06 10:49:16

可以发现有重复的13,发生了覆盖情况。可以用ThreadLocal解决。

public class DateUtil { private static final ThreadLocal<SimpleDateFormat> sdf = //创建时初始化 ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); public static String format(Date date){ return sdf.get().format(date); } }
public class DateUtilTest { //创建多个线程格式化时间 public static void main(String[] args) throws InterruptedException { int count = 10; ExecutorService threadPool = Executors.newFixedThreadPool(count); CountDownLatch latch = new CountDownLatch(count); for (int i = 0; i < count; i++) { // 每个线程任务:构造不同时间,每个相差1秒 final long timeMillis = System.currentTimeMillis() + i * 1000L; threadPool.submit(()->{ try { String format = DateUtil.format(new Date(timeMillis)); System.out.println(Thread.currentThread().getName() + "格式化时间:" + format); } finally { latch.countDown(); } }); } latch.await(); threadPool.shutdown(); } }

运行结果

pool-1-thread-1格式化时间:2026-10-06 10:38:11 pool-1-thread-10格式化时间:2026-10-06 10:38:20 pool-1-thread-2格式化时间:2026-10-06 10:38:12 pool-1-thread-3格式化时间:2026-10-06 10:38:13 pool-1-thread-8格式化时间:2026-10-06 10:38:18 pool-1-thread-4格式化时间:2026-10-06 10:38:14 pool-1-thread-9格式化时间:2026-10-06 10:38:19 pool-1-thread-5格式化时间:2026-10-06 10:38:15 pool-1-thread-6格式化时间:2026-10-06 10:38:16 pool-1-thread-7格式化时间:2026-10-06 10:38:17

可以看到十个线程同时执行没有发生覆盖情况,因为每个线程都有自己的SimpleDateFormat,互不干扰。

上面的例子调用了set,get,那最开始的remove是做什么的?

因为ThreadLocal存在内存泄漏问题,需要手动调用remove防止OOM,下个文章再说。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询