ZetCode

Java ThreadLocal 类

最后修改时间:2025 年 4 月 13 日

java.lang.ThreadLocal 类提供线程局部变量。这些变量与普通变量不同,因为每个线程都有其自身独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段。

当您想维护每个线程的状态而无需同步时,ThreadLocal 非常有用。常见的用例包括存储用户会话、事务上下文或需要与其他线程隔离的其他特定于线程的数据。

ThreadLocal 基础

ThreadLocal 通过为每个线程创建一个单独的变量副本来提供线程隔离。主要方法包括 getsetremoveinitialValue

public class ThreadLocal<T> {
    public T get() {...}
    public void set(T value) {...}
    public void remove() {...}
    protected T initialValue() {...}
}

上面的代码显示了 ThreadLocal 提供的主要方法。泛型类型 T 表示线程局部变量中存储的值的类型。

基本 ThreadLocal 示例

此示例演示了 ThreadLocal 的基本用法。我们创建一个简单的计数器,为每个线程维护单独的计数。

Main.java
package com.zetcode;

public class ThreadLocalExample {
    private static final ThreadLocal<Integer> threadLocalCounter = 
        ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        Runnable task = () -> {
            int counter = threadLocalCounter.get();
            threadLocalCounter.set(counter + 1);
            System.out.println(Thread.currentThread().getName() + 
                ": " + threadLocalCounter.get());
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        Thread thread3 = new Thread(task);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

每个线程都获得自己的计数器副本,初始化为 0。当一个线程增加其计数器时,它不会影响其他线程中的计数器。输出显示每个线程独立维护自己的计数。

带有 InitialValue 的 ThreadLocal

此示例显示了如何覆盖 initialValue 方法,以便为每个线程的变量副本提供自定义初始值。

Main.java
package com.zetcode;

public class ThreadLocalInitialValue {
    private static final ThreadLocal<String> threadLocal = 
        new ThreadLocal<>() {
            @Override
            protected String initialValue() {
                return "Initial-" + Thread.currentThread().getName();
            }
        };

    public static void main(String[] args) {
        Runnable task = () -> {
            System.out.println(Thread.currentThread().getName() + 
                ": " + threadLocal.get());
            threadLocal.set("Modified-" + Thread.currentThread().getName());
            System.out.println(Thread.currentThread().getName() + 
                ": " + threadLocal.get());
        };

        new Thread(task, "Thread-1").start();
        new Thread(task, "Thread-2").start();
    }
}

每个线程都以包含线程名称的自己的初始值开始。然后,我们在每个线程中独立修改该值。输出表明,一个线程中的更改不会影响其他线程的值。

Web 应用程序中的 ThreadLocal

ThreadLocal 通常用于 Web 应用程序中,以存储每个请求的数据,例如用户会话或事务上下文。此示例模拟 Web 请求处理程序。

Main.java
package com.zetcode;

public class WebRequestSimulator {
    private static final ThreadLocal<String> userSession = new ThreadLocal<>();

    static class RequestHandler implements Runnable {
        private final String userId;

        RequestHandler(String userId) {
            this.userId = userId;
        }

        @Override
        public void run() {
            userSession.set("Session-" + userId);
            try {
                System.out.println(Thread.currentThread().getName() + 
                    " processing request for user: " + userId);
                System.out.println("Session ID: " + userSession.get());
                Thread.sleep(100); // Simulate request processing
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                userSession.remove(); // Clean up
            }
        }
    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        for (int i = 1; i <= 5; i++) {
            executor.execute(new RequestHandler("User" + i));
        }
        executor.shutdown();
    }
}

此示例模拟在线程池中处理 Web 请求。每个请求都将其会话存储在 ThreadLocal 中。finally 块确保使用 remove 进行清理,以防止线程池中的内存泄漏。

带有 SimpleDateFormat 的 ThreadLocal

SimpleDateFormat 不是线程安全的,这使得 ThreadLocal 成为跨多个线程共享它的理想解决方案。此示例演示了此模式。

Main.java
package com.zetcode;

public class DateFormatExample {
    private static final ThreadLocal<SimpleDateFormat> dateFormat = 
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

    static class DatePrinter implements Runnable {
        private final Date date;

        DatePrinter(Date date) {
            this.date = date;
        }

        @Override
        public void run() {
            String formattedDate = dateFormat.get().format(date);
            System.out.println(Thread.currentThread().getName() + 
                ": " + formattedDate);
        }
    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        Date today = new Date();

        for (int i = 0; i < 5; i++) {
            executor.execute(new DatePrinter(today));
        }

        executor.shutdown();
    }
}

每个线程都获得自己的 SimpleDateFormat 实例,从而避免了同步问题。这比为每个格式化操作创建一个新的 SimpleDateFormat 或同步对共享实例的访问更有效。

用于上下文传递的 ThreadLocal

ThreadLocal 可用于通过多个方法调用传递上下文,而无需显式传递参数。此示例显示了一个事务上下文。

Main.java
package com.zetcode;

public class TransactionContext {
    private static final ThreadLocal<String> transactionId = new ThreadLocal<>();

    static void startTransaction(String id) {
        transactionId.set(id);
        System.out.println("Started transaction: " + id);
    }

    static void processTransaction() {
        System.out.println("Processing transaction: " + transactionId.get());
    }

    static void commitTransaction() {
        System.out.println("Committing transaction: " + transactionId.get());
        transactionId.remove();
    }

    static class TransactionTask implements Runnable {
        private final String id;

        TransactionTask(String id) {
            this.id = id;
        }

        @Override
        public void run() {
            try {
                startTransaction(id);
                processTransaction();
                commitTransaction();
            } catch (Exception e) {
                transactionId.remove(); // Clean up on error
            }
        }
    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(new TransactionTask("TXN-001"));
        executor.execute(new TransactionTask("TXN-002"));
        executor.shutdown();
    }
}

事务 ID 存储在 ThreadLocal 中,并由多个方法访问,而无需作为参数传递。每个线程都维护自己的事务上下文,并且在事务完成后,ID 会被正确清理。

带有 InheritableThreadLocal 的 ThreadLocal

InheritableThreadLocal 允许子线程从父线程继承值。此示例演示了此行为。

Main.java
package com.zetcode;

public class InheritableThreadLocalExample {
    private static final ThreadLocal<String> regularThreadLocal = 
        new ThreadLocal<>();
    private static final InheritableThreadLocal<String> inheritableThreadLocal = 
        new InheritableThreadLocal<>();

    public static void main(String[] args) {
        regularThreadLocal.set("Parent Thread Value");
        inheritableThreadLocal.set("Parent Thread Value");

        Thread childThread = new Thread(() -> {
            System.out.println("Regular ThreadLocal in child: " + 
                regularThreadLocal.get());
            System.out.println("InheritableThreadLocal in child: " + 
                inheritableThreadLocal.get());
        });

        childThread.start();
    }
}

子线程可以通过 InheritableThreadLocal 访问在父线程中设置的值,但不能通过常规 ThreadLocal 访问。当您需要将上下文传递给子线程,同时保持线程安全时,这非常有用。

ThreadLocal 内存泄漏预防

如果使用不当,ThreadLocal 可能会导致内存泄漏,尤其是在线程池中。此示例显示了正确的清理。

Main.java
package com.zetcode;

public class ThreadLocalCleanup {
    private static final ThreadLocal<byte[]> bigData = new ThreadLocal<>();

    static class Task implements Runnable {
        @Override
        public void run() {
            try {
                // Store large data in ThreadLocal
                bigData.set(new byte[10 * 1024 * 1024]); // 10MB
                System.out.println("Stored data in " + 
                    Thread.currentThread().getName());
            } finally {
                // Critical: remove to prevent memory leak
                bigData.remove();
                System.out.println("Cleaned up in " + 
                    Thread.currentThread().getName());
            }
        }
    }

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

此示例演示了 ThreadLocal 变量的正确清理。即使发生异常,finally 块也能确保删除。如果没有此清理,线程池线程将保留对大型字节数组的引用,从而导致内存泄漏。

来源

Java ThreadLocal 类文档

在本文中,我们通过实际示例介绍了 Java ThreadLocal 类。ThreadLocal 是一个强大的工具,用于管理每个线程的状态而无需同步,但必须小心使用,以避免内存泄漏。

作者

我叫 Jan Bodnar,是一位敬业的程序员,在该领域拥有多年的经验。 我于 2007 年开始撰写编程文章,此后撰写了 1,400 多篇文章和八本电子书。 凭借超过八年的教学经验,我致力于分享我的知识并帮助他人掌握编程概念。

列出所有Java教程