ThreadLocal

1、ThreadLocal是什么及其用途

ThreadLocal是Java语言提供的一种线程局部变量机制,它允许你为每个线程创建变量的私有副本。这意味着每个线程都可以独立地改变自己的变量副本,而不会影响其他线程的变量副本。

用途

ThreadLocal的主要用途包括:

  1. 维护线程封闭性:它可以确保对象的线程局部性,使得并发编程更安全。
  2. 存储线程特有的数据:如用户身份信息、事务状态等。
  3. 性能优化:避免了同步开销,因为每个线程访问自己的ThreadLocal变量不需要加锁。
  4. 简化参数传递:在复杂的调用链中,可以避免通过方法参数传递数据。

深入解析

在Java中,ThreadLocal是通过Thread内部的一个Map实现的,这个Map被称为ThreadLocalMap。每个Thread对象都含有一个ThreadLocal.ThreadLocalMap,而ThreadLocal对象本身作为键,线程局部变量的值作为Map中的值。

下面是ThreadLocal一些核心方法的简化版源码:

ThreadLocalget方法:
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

当调用get()方法时,ThreadLocal会获取当前线程,查找当前线程的ThreadLocalMap,并使用自身作为键来获取值。

ThreadLocalset方法:
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

当调用set()方法时,如果当前线程的ThreadLocalMap存在,就在该Map中保存一个以ThreadLocal实例为键,用户指定值为值的条目;否则,创建该Map并添加该键值对。

ThreadLocalremove方法:
public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null) {
         m.remove(this);
     }
}

调用remove()方法会删除当前线程的ThreadLocalMap中对应的条目,这对于防止内存泄漏非常重要。

代码演示

下面是一个简单的ThreadLocal使用例子:

public class ThreadLocalExample {

    private static final ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 1);

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            System.out.println("Thread " + Thread.currentThread().getName() +
                               " initial value: " + threadLocalValue.get());
            threadLocalValue.set((int) (Math.random() * 100));
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread " + Thread.currentThread().getName() +
                               " new value: " + threadLocalValue.get());
        };
        
        Thread threadOne = new Thread(task);
        Thread threadTwo = new Thread(task);
        threadOne.start();
        threadTwo.start();
        
        threadOne.join();
        threadTwo.join();
    }
}

在这个例子中,两个不同的线程对同一个ThreadLocal变量进行读写操作。每个线程首先打印出ThreadLocal的初始值,然后设置一个随机值,并再次打印出来。由于使用了ThreadLocal,尽管两个线程访问相同的threadLocalValue变量,它们所看到的值却是彼此隔离的。

注意事项

使用ThreadLocal时,需要特别注意内存泄漏问题。由于每个线程的ThreadLocalMapThreadLocal实例的键的引用是弱引用(WeakReference),但值的引用是常规引用,如果ThreadLocal不再被外部引用,而线程还在运行,则ThreadLocal实例的键可能会被垃圾回收,但值却不会。如果这个值是一个重对象,或者持有对其他对象的重引用,就可能发生内存泄漏。因此,最好在不再需要使用ThreadLocal变量时,显式调用remove()方法来清理资源。

2、ThreadLocal如何实现线程隔离?

ThreadLocal通过为每个线程提供一个独立的变量副本来实现线程隔离。这是通过在每个线程中维护一个称为ThreadLocalMap的内部结构来完成的,该结构存储了线程特定的值。

ThreadLocalMap原理

ThreadLocalMap是一个定制的哈希映射,只能由包含它的Thread对象访问。Thread对象中有一个称为threadLocals的字段,这是一个指向ThreadLocalMap的引用,但这个字段是包访问保护的,外部代码无法直接访问。

每个ThreadLocal对象可以被视为ThreadLocalMap的键,而与键关联的值就是线程特定的值。在实际实现中,为了避免内存泄漏,键是通过弱引用存储的,这意味着键可以被垃圾收集器回收。

ThreadLocal方法源码

在Java源码的ThreadLocal类中,get()set()方法主要负责获取和设置线程特定的值:

ThreadLocal.get()实现:
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

get()方法首先获取当前线程实例,然后获取与该线程关联的ThreadLocalMap。它使用this(当前ThreadLocal对象)作为键来查找相关的值。如果找到值,则返回该值。如果ThreadLocalMap不存在或没有找到值,它将调用setInitialValue()方法来创建并返回初始值。

ThreadLocal.set()实现:
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

set()方法同样获取当前线程实例,然后获取或创建ThreadLocalMap并将值与ThreadLocal对象关联起来。

代码演示

下面是一个使用ThreadLocal的代码示例,展示了如何为每个线程创建和维护自己的值副本:

public class ThreadLocalExample {
    private static final ThreadLocal<Integer> threadId =
        new ThreadLocal<Integer>() {
            final AtomicInteger nextId = new AtomicInteger(0);
            protected Integer initialValue() {
                return nextId.getAndIncrement();
            }
        };

    public static int getThreadId() {
        return threadId.get();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            new Thread(() -> {
                System.out.printf("Thread #%d has task id %d%n", getThreadId(), taskId);
            }).start();
        }
    }
}

在这个例子中,我们定义了一个ThreadId,它为每个线程生成一个唯一的ID。每个线程在开始执行时都会调用getThreadId(),该方法返回与当前线程关联的ID,initialValue()方法确保每个线程都有一个唯一的ID。

注意事项

  • 内存泄漏:因为ThreadLocalMap的生命周期与它所属的线程一样长,如果ThreadLocal变量没有被移除,而线程又一直存在(如在线程池中),ThreadLocalMap和它的内容就不会被垃圾回收,从而可能导致内存泄漏问题。故显式调用ThreadLocal.remove()是个好习惯。

  • 弱引用ThreadLocalMap中的键是通过弱引用存储的,以允许ThreadLocal对象被回收,而不会被ThreadLocalMap的引用阻止。但是值并不是以弱引用存储的,因此如果值指向的对象很大,就必须确保调用remove()来防止内存泄漏。

3、如何使用ThreadLocal?

ThreadLocal 通过为每个线程提供一个线程局部(Thread-Local)存储空间,允许开发者为每个线程存储数据,而这些数据对其他线程而言是隔离的。ThreadLocal 实例通常是类中的私有静态字段,它们关联着与使用该变量的线程相关的值。

使用 ThreadLocal 的基本步骤:

  1. 创建 ThreadLocal 变量: 声明一个 ThreadLocal 类型的静态变量。你可以通过覆盖 initialValue 方法来为 ThreadLocal 变量提供一个初始值,或者使用 withInitial 工厂方法。

  2. 存储和访问 ThreadLocal 数据: 使用 get() 方法来访问当前线程相关联的值。如果该值尚未设置,则会返回initialValue 方法设定的值。使用 set() 方法可以设置当前线程的局部变量的值。

  3. 清理 ThreadLocal 数据: 在不再需要存储在 ThreadLocal 中的数据时,需要调用 remove() 方法来避免潜在的内存泄漏,特别是在使用线程池时。

源码分析

ThreadLocal 的实现中,最关键的一部分就是内部类 ThreadLocalMap,它是一个定制的哈希映射,用于存储线程局部变量的值。

ThreadLocal.ThreadLocalMap

ThreadLocalMap 是一个简化的自定义哈希表,只适用于维护线程局部变量。它使用 ThreadLocal 实例作为键,线程局部存储的对象作为值。

ThreadLocal.set(T value) 方法

set() 方法将当前 ThreadLocal 实例与值相关联在当前线程的 ThreadLocalMap 中:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = t.threadLocals;
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}
ThreadLocal.get() 方法

get() 方法用于获取与当前线程相关联的 ThreadLocal 实例的值:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = t.threadLocals;
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            return (T)e.value;
        }
    }
    return setInitialValue();
}

如果当前线程的 ThreadLocalMap 不存在或者没有找到对应的值,setInitialValue() 会被调用以初始化值。

代码演示

public class ThreadLocalExample {

    private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        
        Runnable task = () -> {
            String formattedDate = dateFormatThreadLocal.get().format(new Date());
            System.out.println("Thread: " + Thread.currentThread().getName() + ", Formatted Date: " + formattedDate);
        };
        
        // 启动两个线程,执行任务
        executor.submit(task);
        executor.submit(task);
        
        // 关闭 ExecutorService 并等待其终止
        executor.shutdown();
    }
}

在这个例子中,我们创建了一个 ThreadLocal 实例,用于存储 SimpleDateFormat 对象。每个线程将获取自己的日期格式化实例,并使用它来格式化当前日期。在多线程环境中,使用 ThreadLocal 可以确保每个线程都有自己的 SimpleDateFormat 实例,从而避免了线程安全问题。

  • ThreadLocalinitialValue() 方法在第一次调用 get() 时被触发,如果尚未调用 set(),则用于提供初始值。
  • 在使用线程池时,线程通常会被重用,因此在任务结束时调用 remove() 方法清理 ThreadLocal 是一个很好的习惯,以免导致内存泄漏。
  • ThreadLocal 不是用来解决共享对象的多线程访问问题的,而是为了提供线程内部的私有存储。对于多线程访问共享资源的同步控制,应该使用其他同步机制,比如 synchronizedReentrantLock 等。
  • ThreadLocal 可以减少对于某些需要线程安全的对象的同步需求(如上例中的 SimpleDateFormat),因为每个线程都有自己的实例,从而提高执行效率。

4、ThreadLocal的常用方法有哪些?

ThreadLocal 类在 Java 中提供了一组设计用来操作线程局部变量的方法。这些方法可以分为几个类别:初始化、访问、修改和清除线程局部变量。

初始化方法

  • initialValue() :这是一个受保护的方法,通常由你继承 ThreadLocal 并覆盖该方法来供 get() 方法在首次访问线程局部变量时使用,以便提供一个初始值。
protected T initialValue() {
    return null;
}
  • withInitial(Supplier supplier) :这是一个静态工厂方法,Java 8 引入,允许你通过传递一个 Supplier 函数式接口的实例来创建一个 ThreadLocal 对象,并且用 Supplier 提供的值作为初始值。
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

访问和修改方法

  • get() :返回与当前线程关联的此线程局部变量的值。如果变量没有初始化,则会调用 initialValue() 方法来进行初始化。
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocal.ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            return (T)e.value;
        }
    }
    return setInitialValue();
}
  • set(T value) :将当前线程的此线程局部变量的副本设置为指定的值。
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocal.ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

清除方法

  • remove() :移除当前线程的此线程局部变量的值。
public void remove() {
     ThreadLocal.ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null) {
         m.remove(this);
     }
}

代码演示

下面是如何使用 ThreadLocal 的一个实例,演示了如何为每个线程存储和访问一个唯一的用户ID。

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadLocalExample {

    private static final ThreadLocal<Integer> userIdThreadLocal = ThreadLocal.withInitial(new AtomicInteger()::getAndIncrement);

    public static void main(String[] args) {
        Runnable task = () -> {
            int userId = userIdThreadLocal.get();
            System.out.println("Thread " + Thread.currentThread().getName() + " User ID: " + userId);
        };

        // 启动三个线程
        new Thread(task).start();
        new Thread(task).start();
        new Thread(task).start();
    }
}

这个例子中每次运行任务,userIdThreadLocal.get() 都会返回一个唯一的用户ID,这是因为我们利用 AtomicInteger 提供了一个线程安全的自增操作。

在使用 ThreadLocal 方法时需要注意几个重要点:

  • 内存泄漏问题:每个线程都有一个对应的 ThreadLocal.ThreadLocalMap 实例,如果线程一直运行着,而且我们没有及时调用 remove() 方法,那么由于 ThreadLocalMap 对这些局部变量的强引用,可能会导致内存泄漏。这在使用线程池时尤其重要,因为线程通常会被重用。

  • 初始值initialValue()withInitial(Supplier supplier) 方法可以确保每个线程都有自己的变量初始值。如果没有提供这些方法,线程局部变量的初始值将为 null

  • 线程局部变量的数据隔离ThreadLocal 变量确保数据的线程隔离性,这意味着每个线程都能独立地操作自己的数据副本,不受其他线程的影响。

  • 类型安全ThreadLocal 是一个泛型类,使得线程局部变量是类型安全的。

总之,ThreadLocal 是管理线程局部变量的强大工具,但也应该要注意其使用方式,避免潜在的内存泄漏问题。

5、如何清理ThreadLocal资源?

清理 ThreadLocal 资源是防止内存泄漏的重要步骤。由于 ThreadLocal 在每个线程中为变量维护单独的副本,如果这些线程是长时间运行的,或者是线程池中的线程,那么这些变量可能会在不再需要它们之后仍然存活,造成内存泄漏。以下是如何清理 ThreadLocal 资源的详细说明:

ThreadLocal 资源释放流程

  1. 使用 ThreadLocal.remove() 方法:
    当你知道不再需要某个线程局部变量时,应该调用 ThreadLocal 实例的 remove() 方法来移除当前线程的这个变量副本。该操作会从当前线程的 ThreadLocalMap 中移除对应的条目。

    源码解析:

    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
    }
    

    该方法首先取得当前线程的 ThreadLocalMap,然后调用 remove() 方法删除当前 ThreadLocal 实例的键(及其对应的值)。

  2. 确保线程结束时清理:
    在使用完 ThreadLocal 变量后,应该尽早调用 remove。这在使用线程池的场景下尤为重要,因为线程池中的线程通常不会结束,而是被重用,所以它们引用的 ThreadLocal 变量可能不会自动被垃圾回收。

清理 ThreadLocal 资源的示例

public class ThreadLocalCleanupExample {

    private static final ThreadLocal<SimpleDateFormat> dateFormatter = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }

        @Override
        public void remove() {
            super.remove();
            System.out.println("DateFormatter removed for Thread: " + Thread.currentThread().getName());
        }
    };

    public static void main(String[] args) {
        Runnable task = () -> {
            String dateStamp = dateFormatter.get().format(new Date());
            System.out.println("Thread: " + Thread.currentThread().getName() + ", Date Stamp: " + dateStamp);
            // 在线程逻辑的最后调用 remove 来清理
            dateFormatter.remove();
        };

        ExecutorService executorService = Executors.newFixedThreadPool(3);
        try {
            // 执行任务
            executorService.execute(task);
            executorService.execute(task);
            executorService.execute(task);
        } finally {
            executorService.shutdown();
        }
    }
}

在上面的示例中,我们首先定义了一个 ThreadLocal 变量 dateFormatter,然后在多线程的环境下使用它。在每个线程执行的任务的最后,我们调用了 dateFormatter.remove() 来清理每个线程中的 ThreadLocal 资源。

注意事项

  • 明确 remove 调用时机: 最佳实践是,在每次使用完 ThreadLocal 变量之后立即调用 remove() 方法。如果该变量是在 try 块中使用的,那么 remove() 应该在 finally 块中调用以确保它总是执行。

  • 防止内存泄漏: 如果不在每个线程结束时清理线程局部变量,尤其是在线程池场景中,ThreadLocal 变量可能会导致内存泄漏,因为 ThreadLocalMap 的生命周期与线程一样长。

  • 了解 ThreadLocal 内部机制: ThreadLocal 使用 Thread 对象中的 threadLocals 字段来存储每个线程的 ThreadLocalMap。每个 ThreadLocalMap 使用线程安全的方法存储和检索键值对。

通过遵循这些步骤和注意事项,你可以确保在使用 ThreadLocal 时及时清理资源,避免内存泄漏,并养成一个良好的编程习惯。

6、ThreadLocal有哪些缺点?

ThreadLocal 提供了一个方便的方式来在每个线程中管理数据的隔离性,但是它也有一些缺点和局限性:

1. 内存泄漏的风险

ThreadLocal 在线程的生命周期中持有变量,如果在使用完后不显式地调用 ThreadLocal.remove() 方法来清除这些变量,它们可能会一直存活在 JVM 的内存中,尤其是在使用线程池时,因为线程池中的线程通常会被重用而不是结束。

源码分析

这是因为每个 Thread 对象都有一个 ThreadLocal.ThreadLocalMap 的引用,它使用 ThreadLocal 对象作为键,存储了线程局部变量的值。由于 ThreadLocalMap 使用 ThreadLocal 对象的弱引用作为键,所以即使 ThreadLocal 对象不再被引用,它的键可以被垃圾收集器回收。但是,值不是弱引用,如果没有显式调用 remove() 方法,值就不能被回收。

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    // ...
}

2. 增加线程创建的成本

每个线程创建时都会创建与之关联的 ThreadLocalMap,这就意味着如果系统创建了大量的线程,每个线程又使用了大量的 ThreadLocal 变量,那么管理这些额外的数据结构会增加额外的内存负担。

3. 可能导致不同模块间的数据传递不清晰

因为 ThreadLocal 变量对于它们所在的线程是私有的,它们可以在整个线程的执行过程中任何地方被访问,这可能导致代码的维护和理解变得复杂,尤其是在大型项目和团队中。

4. 对于并发工具的支持不友好

使用 fork/join 框架或者 java.util.stream 中的并行流时,ThreadLocal 变量可能会引起混乱,因为这些并发框架可能会将任务分配给线程池中的任何线程,而这些线程可能已经携带了一些 ThreadLocal 变量的值,从而使得数据的状态变得不可预测。

代码演示

public class ThreadLocalDisadvantages {

    private static final ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();

    public static void main(String[] args) {
        threadLocalValue.set(1);
        new Thread(() -> {
            // 没有调用threadLocalValue.set(),但是可能获取到错误的值
            // 如果在其他地方有修改threadLocalValue,结果是不可预测的
            System.out.println(threadLocalValue.get()); // 可能打印null
        }).start();
        
        // ... 执行其他操作
        
        // 忘记调用 remove(),可能导致内存泄漏
        // threadLocalValue.remove(); // 应该显式调用
    }
}

在这个示例中,如果忘记调用 threadLocalValue.remove(),那么随着线程的结束,由于没有及时清理资源,将可能导致内存泄漏。

在考虑使用 ThreadLocal 时,你应该明白:

  • 需要负责清理你设置的任何 ThreadLocal 变量,以防内存泄漏。
  • 尽量减少在一个给定的线程中使用 ThreadLocal 变量的数量,以减轻对内存的压力。
  • 仅在需要维护线程隔离的特定情况下使用 ThreadLocal,并且注意它可能会使你的代码更难维护。
  • 当你的应用程序在使用如 CompletableFutureparallelStream 等并发工具时,谨慎使用 ThreadLocal,因为这些工具可能会与 ThreadLocal 变量产生不一致的行为。

合理使用 ThreadLocal 确实可以解决特定的问题,但也应该意识到它的缺点并采取措施避免相关问题。

7、ThreadLocal与同步机制(例如synchronized,Lock)有什么区别?

ThreadLocal 和同步机制(如 synchronizedLock)都用于多线程编程环境,但它们解决的问题和使用方式有很大的不同。

ThreadLocal

ThreadLocal 用来为每个线程提供各自的变量副本,确保线程之间的数据隔离。每个线程都可以以线程安全的方式访问其内部 ThreadLocalMap 中的变量,而无需进行同步,因为这些变量对其他线程来说是不可见的。

示例代码
public class ThreadLocalExample {
    private static final ThreadLocal<Integer> threadId =
        ThreadLocal.withInitial(() -> Thread.currentThread().hashCode());

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("Thread A ID: " + threadId.get()); // 线程A的独立ID
        }).start();

        new Thread(() -> {
            System.out.println("Thread B ID: " + threadId.get()); // 线程B的独立ID
            threadId.remove();
        }).start();
    }
}

在这里,每个线程调用 threadId.get() 时都会得到一个与其他线程不同的值。由于使用的是 ThreadLocal 变量,无需担心多线程访问冲突,因此不使用同步机制。

同步机制

同步机制(如 synchronizedLock)是为了在多个线程之间安全地共享资源。当多个线程想要访问同一个资源时,同步机制确保同一时间只有一个线程可以访问资源,防止并发问题如数据竞争和条件竞争。

synchronized 示例代码
public class SynchronizedExample {
    private static int sharedState = 0;

    public static synchronized void increment() {
        sharedState++;
    }

    public static void main(String[] args) {
        Thread threadA = new Thread(SynchronizedExample::increment);
        Thread threadB = new Thread(SynchronizedExample::increment);
        threadA.start();
        threadB.start();
    }
}

在上面的代码中,increment() 方法是 synchronized 的,这意味着同一时间只有一个线程能够修改 sharedState

Lock 示例代码
import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    private static final ReentrantLock lock = new ReentrantLock();
    private static int sharedState = 0;

    public static void increment() {
        lock.lock();
        try {
            sharedState++;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Thread threadA = new Thread(LockExample::increment);
        Thread threadB = new Thread(LockExample::increment);
        threadA.start();
        threadB.start();
    }
}

Lock 示例中,我们使用 ReentrantLock 明确地管理锁定和解锁过程,以确保 sharedState 变量的线程安全。

区别分析

  • 目的不同ThreadLocal 是为了隔离数据,让每个线程有自己的数据副本。同步机制是为了保护数据,不允许多个线程同时修改同一份数据。
  • 使用场景不同:如果你需要在多线程环境下保持变量的独立副本,你应该使用 ThreadLocal。如果你需要协调多个线程对共享资源的访问,你应该使用同步机制。
  • 性能影响ThreadLocal 可能会导致更高的内存消耗,但通常不会引起线程阻塞。同步机制可能会引起线程竞争、阻塞和上下文切换,这可能对性能有显著影响。

在选择使用 ThreadLocal 还是同步机制时,你应该考虑以下因素:

  • 线程安全需求:是否需要每个线程有自己的变量副本?还是需要多个线程同步访问共享资源?
  • 资源共享程度:变量是否被多个线程共享?共享的程度是什么?
  • 性能要求:应用程序的性能需求是什么?是否有潜在的性能瓶颈?
  • 内存要求:应用程序的内存限制是什么?是否可以承受 ThreadLocal 可能带来的额外内存消耗?

通过理解这些关键差异并根据实际的应用场景和需求作出选择,可以更有效地使用这些工具来编写线程安全的代码。

8、ThreadLocal和InheritableThreadLocal有什么区别?

ThreadLocalInheritableThreadLocal 都是用来提供线程范围内的变量存储,但是它们在处理线程之间的变量继承方面存在差异。

ThreadLocal

ThreadLocal 为每个使用该变量的线程提供了一个独立初始化的变量副本。每个线程只能访问和修改自己的副本,而不会影响其他线程中的副本。这对于保存线程私有的状态非常有用。

ThreadLocal 源码中的核心部分
public class ThreadLocal<T> {
    // ...
    
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
    // ...
}

ThreadLocal 使用 ThreadLocalMap,是 Thread 对象的一个内部类,它使用 ThreadLocal 实例本身作为键来存储值。

InheritableThreadLocal

InheritableThreadLocal 扩展了 ThreadLocal,提供了一种新的线程局部变量,它不仅可以被线程本身访问,还可以被该线程派生出的子线程访问。当一个线程创建一个新的子线程时,InheritableThreadLocal 会将父线程中的值复制到子线程中。

InheritableThreadLocal 源码中的核心部分
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    // ...
    
    protected T childValue(T parentValue) {
        return parentValue;
    }
    
    // ...
}

当一个新的线程被创建时,Java 虚拟机会调用 Thread 类的构造函数,这个过程中会检查父线程的 inheritableThreadLocals 字段,并将其通过 childValue 方法复制到新创建的线程。

继承机制的处理
Thread parentThread = new Thread(() -> {
    InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
    inheritableThreadLocal.set("value");

    Thread childThread = new Thread(() -> {
        // 子线程可以访问由父线程设置的inheritableThreadLocal的值
        System.out.println("Child thread value: " + inheritableThreadLocal.get()); // 输出 "value"
    });
    childThread.start();
});

parentThread.start();

子线程将输出由父线程设置在 InheritableThreadLocal 上的值。

区别分析

  • 变量继承ThreadLocal 不会将值传递给任何派生的子线程,而 InheritableThreadLocal 设计用来做到这一点。
  • 使用场景:当你需要保持父线程和子线程之间的某些状态共享时,可以使用 InheritableThreadLocal。相比之下,如果你想要确保每个线程的数据是完全隔离的,那么 ThreadLocal 是更合适的选择。

性能和资源管理

  • 内存泄漏的风险:两者都具有内存泄漏的风险,如果不适时调用 remove() 方法进行清理。
  • 性能开销InheritableThreadLocal 的复制操作可能会带来额外的性能开销,尤其是当创建大量线程时。

在使用 InheritableThreadLocal 时,你应该注意:

  • 适用性:它适用于你确实需要在父子线程之间共享数据的场景。
  • 潜在问题:在使用线程池时,由于线程通常会被重用,这可能导致 InheritableThreadLocal 中的值在不同任务之间产生混淆。
  • 值覆盖:子线程可以覆盖从父线程继承的值,而且如果子线程更改了这个值,父线程是看不到这个更改的。

总结来说,ThreadLocalInheritableThreadLocal 分别适用于不同的场景,开发者应该根据具体的需求和线程模型来选择使用哪一个。同时,要注意它们的使用需要谨慎管理,以避免内存泄漏等问题。

9、使用ThreadLocal有哪些最佳实践?

使用 ThreadLocal 可以让每个线程都拥有自己的变量副本,但如果不恰当地使用,可能会引发内存泄漏等问题。以下是使用 ThreadLocal 的一些最佳实践:

1. 及时清理资源

在不再需要存储在 ThreadLocal 中的数据时,应当显式调用 ThreadLocal.remove() 来清理资源。这是因为 ThreadLocal 可能会导致长生命周期的线程(例如线程池中的线程)持续持有对放在 ThreadLocal 中对象的引用,从而引发内存泄漏。

示例代码
ThreadLocal<MyObject> myThreadLocal = new ThreadLocal<>();
try {
    myThreadLocal.set(new MyObject());
    // 执行一些操作
} finally {
    myThreadLocal.remove();
}

在上述示例中,确保在 finally 块中调用 .remove() 以避免内存泄漏。

2. 限制 ThreadLocal 变量的作用域

通常情况下,ThreadLocal 变量应该是 private static 的,以减少对这些变量的可见性,并避免在多个类或对象之间共享它们。

3. 使用 withInitial 工厂方法

Java 8 引入的 ThreadLocal.withInitial(Supplier supplier) 工厂方法可以方便地设置初始值。

示例代码
private static final ThreadLocal<DateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));

4. 避免在 ThreadLocal 中存储大型对象

由于 ThreadLocal 数据是与线程绑定的,存储大型对象可能会导致内存消耗过大,特别是在使用大量线程的应用程序中。

5. 在 InheritableThreadLocal 使用时注意线程池问题

如果你正在使用 InheritableThreadLocal 和线程池,请注意,由于线程池线程的复用,设置的值可能会在多次任务执行间意外共享。

6. 使用 ThreadLocal 映射替代多个 ThreadLocal 变量

如果你需要在一个线程中存储多个线程局部变量,可以考虑使用单个 ThreadLocal,其中存储一个映射(Map)来存放多个键值对。

7. 理解 ThreadLocal 的工作原理

深入理解 ThreadLocal 的实现,可以帮助更好地使用它。每个线程持有一个 ThreadLocal.ThreadLocalMap 的引用,而 ThreadLocalMap 使用线性探测哈希映射(linear probing hash map)来解决键的冲突。

ThreadLocalMap 源码关键部分
static class ThreadLocalMap {
    // Entry 继承自 WeakReference,用于引用 thread-local 变量的键
    static class Entry extends WeakReference<ThreadLocal<?>> {
        // 值不是弱引用
        Object value;
        
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    // ...
}

8. 谨慎使用 ThreadLocal 在 servlet 环境中

在 servlet 环境中使用 ThreadLocal 是需要特别注意的,因为 web 容器可能会使用线程池,这意味着线程可能在处理多个请求之间被重用。确保在请求处理完成之后清理 ThreadLocal 变量。

总结

ThreadLocal 是一个强大的工具,可以帮助解决特定的线程隔离问题。然而,如果不遵守上述最佳实践,它可能会导致严重的资源管理问题,如内存泄漏。正确地使用 ThreadLocal 可以确保你的应用程序的性能和可维护性。

你可能感兴趣的:(java,后端)