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,下个文章再说。