【并发】ThreadLocal 遇上线程池的问题及解决办法

11 篇文章 0 订阅
4 篇文章 0 订阅

https://www.cnblogs.com/qifenghao/p/8977378.html

ThreadLocal 称为线程本地存储,一般作为静态域使用,它为每一个使用它的线程提供一个其值(value)的副本。通常对数据库连接(Connection)和事务(Transaction)使用线程本地存储。

可以简单地将 ThreadLocal 理解成一个容器,它将 value 对象存储在 Map<Thread, T> 域中,即使用当前线程为 key 的一个 Map,ThreadLocal 的 get() 方法从 Map 里取与当前线程相关联的 value 对象。ThreadLocal 的真正实现并不是这样的,但是可以简单地这样理解

线程池中的线程在任务执行完成后会被复用,所以在线程执行完成时,要对 ThreadLocal 进行清理(清除掉与本线程相关联的 value 对象)。不然,被复用的线程去执行新的任务时会使用被上一个线程操作过的 value 对象,从而产生不符合预期的结果。
下面举一个简单的例子来说明:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadLocalVariableHolder {
    private static ThreadLocal<Integer> variableHolder = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
    public static int getValue() {
        return variableHolder.get();
    }
    public static void remove() {
        variableHolder.remove();
    }
    public static void increment() {
        variableHolder.set(variableHolder.get() + 1);
    }
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            executor.execute(() -> {
                int before = getValue();
                increment();
                int after = getValue();
                System.out.println("before: " + before + ", after: " + after);
            });
        }
        executor.shutdown();
    }
}

执行结果如下(如果你的执行结果 before 都是 0,after 都是 1 的话,就增加线程池执行的线程个数):

before: 0, after: 1
before: 0, after: 1
before: 0, after: 1
before: 1, after: 2
before: 1, after: 2

既然是为每个线程都提供一个副本,为什么会出现 before 不为 0 的情况呢?
下面追踪每一个执行的线程,将 main 方法修改为如下:

public static void main(String[] args) {
    ExecutorService executor = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
        executor.execute(() -> {
            long threadId = Thread.currentThread().getId();
            int before = getValue();
            increment();
            int after = getValue();
            System.out.println("threadId: " + threadId + ", before: " + before + ", after: " + after);
        });
    }
    executor.shutdown();
}

执行结果如下:

threadId: 10, before: 0, after: 1
threadId: 11, before: 0, after: 1
threadId: 12, before: 0, after: 1
threadId: 12, before: 1, after: 2
threadId: 11, before: 1, after: 2

由上面的执行结果可以看出,id 为 11 和 12 的线程被复用。线程池在复用线程执行任务时使用被之前的线程操作过的 value 对象。因此,在每个线程执行完成时,应该清理 ThreadLocal。具体做法如下:

public static void main(String[] args) {
    ExecutorService executor = Executors.newCachedThreadPool();
    for (int i = 0; i < 100; i++) {
        executor.execute(() -> {
            try {
                long threadId = Thread.currentThread().getId();
                int before = getValue();
                increment();
                int after = getValue();
                System.out.println("threadId: " + threadId + ", before: " + before + ", after: " + after);
            } finally {
                // 清理线程本地存储
                remove();
            }
        });
    }
    executor.shutdown();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值