一文上手ThreadLocal使用和原理

什么是ThreadLocal?它有什么用?

当我们某个类需要被多线程共享的时候,我们就可以使用ThreadLocal关键字,ThreadLocal可以为每个线程创建这个变量的副本并存到每个线程的存储空间中(关于这个存储空间后文会展开讲述),从而确保共享变量对每个线程隔离,实现线程安全。

在这里插入图片描述

举个例子,我们现在有一个应用,应用中有1000个线程,每个线程都会对数据库进行操作,于是我们编写了一个数据库连接工具类:

class ConnectionManager {
	//静态变量,所有线程共享一个connect 
    private static Connection connect = null;

    public static Connection openConnection() {
    	//
        if (connect == null) {
            connect = DriverManager.getConnection();
        }
        return connect;
    }

    public static void closeConnection() {
        if (connect != null)
            connect.close();
    }
}

代码很简单,问题也很明显,假如我们1000个线程同时去调用这个工具类建立数据库连接,很可能在同一个时间,同时指向getConnection方法。

在这里插入图片描述

亦或者在getConnection方法期间,某个线程调用closeConnection导致其他线程意外崩溃。

在这里插入图片描述

所以为了保证没必要的重复创建,我们对代码进行改良,将connect 变为普通成员变量。

class ConnectionManager {
    private Connection connect = null;

    public Connection openConnection() {
        if (connect == null) {
            connect = DriverManager.getConnection();
        }
        return connect;
    }

    public void closeConnection() {
        if (connect != null)
            connect.close();
    }
}

虽然保证了线程安全,但是问题也来了,每个线程在进行SQL操作时都需要调用openConnection,假如我们1000个线程反复执行SQL操作,如此频繁的openConnection、openConnection将严重影响服务器性能。

在这里插入图片描述

有没有什么办法,可以让线程留住这个变量,下次使用该线程的使用能够复用这个连接呢?

答案就是ThreadLocal,我们对上述代码在进行一次改造

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectionManager {

    private static final ThreadLocal<Connection> dbConnectionLocal = new ThreadLocal<Connection>() {
		//每个线程初次调用dbConnectionLocal.get()时,就会在自己内部存储DriverManager.getConnection("", "", "")。
        @Override
        protected Connection initialValue() {
            try {
                return DriverManager.getConnection("", "", "");
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return null;
        }
    };

    public Connection getConnection() {
        return dbConnectionLocal.get();
    }
}

我们通过ThreadLocal变量dbConnectionLocal确保每个线程调用getConnection方法时,通过dbConnectionLocal.get()调用initialValue(这个调用流程后续会解释),实现在这个线程内部存储一个Connection 对象,只要我们没有清除或者关闭当前线程,这个Connection 对象就可以一直使用,且线程之间互不干扰。

在这里插入图片描述

ThreadLocal基础使用示例

如下所示,基于ThreadLocal为每个将每个线程的id存到线程内部,彼此之间互不影响。

//THREAD_LOCAL变量
    private static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    private static Logger logger = LoggerFactory.getLogger(ThreadLocalTest.class);

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            logger.info("t1往THREAD_LOCAL存入变量:[{}]", Thread.currentThread().getName());
            THREAD_LOCAL.set(Thread.currentThread().getName());
            logger.info("t1获取THREAD_LOCAL的值为:[{}]", THREAD_LOCAL.get());
        }, "t1");


        Thread t2 = new Thread(() -> {
            logger.info("t2往THREAD_LOCAL存入变量:[{}]", Thread.currentThread().getName());
            THREAD_LOCAL.set(Thread.currentThread().getName());
            logger.info("t2获取THREAD_LOCAL的值为:[{}]", THREAD_LOCAL.get());
            THREAD_LOCAL.remove();
            logger.info("t2删除THREAD_LOCAL的后值为:[{}]", THREAD_LOCAL.get());
        }, "t2");

        t1.start();
        t2.start();
    }

从输出结果可以看出,两个线程都用THREAD_LOCAL 在自己的内存空间中存储了变量的副本,彼此互相隔离的使用

[t1] INFO com.guide.thread.base.ThreadLocalTest - t1往THREAD_LOCAL存入变量:[t1]
[t1] INFO com.guide.thread.base.ThreadLocalTest - t1获取THREAD_LOCAL的值为:[t1]
[t2] INFO com.guide.thread.base.ThreadLocalTest - t2往THREAD_LOCAL存入变量:[t2]
[t2] INFO com.guide.thread.base.ThreadLocalTest - t2获取THREAD_LOCAL的值为:[t2]
[t2] INFO com.guide.thread.base.ThreadLocalTest - t2删除THREAD_LOCAL的后值为:[null]

从两种应用场景来介绍一下ThreadLocal

日期格式化工具类

错误示例

代码如下,我们创建100个线程使用同一个dateFormat完成日期格式化

 private static Logger logger = LoggerFactory.getLogger(MyThreadLocalDemo3.class);


    static SimpleDateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS");


    public static void main(String[] args) throws InterruptedException {

        ExecutorService threadPool = Executors.newFixedThreadPool(100);

        for (int i = 0; i < 100; i++) {
            int finalI = i;
            //线程池中的线程
            threadPool.submit(()->{
                new MyThreadLocalDemo3().caclData(finalI);

            });
        }

        threadPool.shutdown();

    }

    /**
     * 计算second后的日期
     * @param second
     * @return
     */
    public String caclData(int second){
        Date date=new Date(1000*second);
        String dateStr = dateFormat.format(date);
        logger.info("{}得到的时间字符串为:{}",Thread.currentThread().getId(),dateStr);
        return dateStr;
    }

从输出结果可以看出,间隔几毫秒的线程出现相同结果

在这里插入图片描述

使用ThreadLocal为线程分配SimpleDateFormat副本
static ThreadLocal<SimpleDateFormat> threadLocal=ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS"));


    public static void main(String[] args) throws InterruptedException {

        ExecutorService threadPool = Executors.newFixedThreadPool(100);

        for (int i = 0; i < 100; i++) {
            int finalI = i;
            //线程池中的线程
            threadPool.submit(()->{
                new MyThreadLocalDemo3().caclData(finalI);

            });
        }

        threadPool.shutdown();

    }

    /**
     * 计算second后的日期
     * @param second
     * @return
     */
    public String caclData(int second){
        Date date=new Date(1000*second);
        SimpleDateFormat simpleDateFormat = threadLocal.get();
        String dateStr = simpleDateFormat.format(date);
        logger.info("{}得到的时间字符串为:{}",Thread.currentThread().getId(),dateStr);
        return dateStr;
    }

web服务层共享变量

我们日常web开发都会涉及到各种service的调用,例如某个controller需要调用完service1之后再调用service2。
因为我们的controller和service都是单例的,所以如果我们希望多线程调用这些controller和service保证共享变量的隔离,也可以用到ThreadLocal。

为了实现这个示例,我们编写了线程获取共享变量的工具类。

package threadlocal;

import java.text.SimpleDateFormat;

public class MyUserContextHolder {
    private static ThreadLocal<User> holder = new ThreadLocal<>();

    public static ThreadLocal<User> getHolder() {
        return holder;
    }
}

service调用链示例如下,笔者创建service1之后,所有线程复用这个service完成了调用。

public class MyThreadLocalGetUserId {

    private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);

    private static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            MyService1 service1 = new MyService1();
            threadPool.submit(() -> {

                service1.doWork1("username" + (finalI+1));
            });

        }


    }
}


class MyService1 {

    private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);

    public void doWork1(String name) {

        logger.info("service1 存储userName:" + name);
        ThreadLocal<String> holder = MyUserContextHolder.getHolder();
        holder.set(name);
        MyService2 service2 = new MyService2();
        service2.doWork2();
    }

}

class MyService2 {
    private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);

    public void doWork2() {
        ThreadLocal<String> holder = MyUserContextHolder.getHolder();

        logger.info("service2 获取userName:" + holder.get());
        MyService3 service3 = new MyService3();
        service3.doWork3();
    }
}


class MyService3 {
    private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);

    public void doWork3() {
        ThreadLocal<String> holder = MyUserContextHolder.getHolder();

        logger.info("service3获取 userName:" + holder.get());

// 避免oom问题
        holder.remove();
    }
}

从输出结果来看,在单例对象情况下,既保证了同一个线程间变量共享。

在这里插入图片描述

也保证了不同线程之间变量的隔离。

在这里插入图片描述

基于源码了解ThreadlLocal工作原理

ThreadlLocal如何做到线程隔离的?

我们不妨以上面日期格式化工具为例子,从源码的角度分析问题,我们先回顾一下ThreadLocal的定义

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

然后是基于ThreadLocal实现的计算方法

/**
     * 计算second后的日期
     * @param second
     * @return
     */
    public String caclData(int second){
        Date date=new Date(1000*second);
        SimpleDateFormat simpleDateFormat = threadLocal.get();
        String dateStr = simpleDateFormat.format(date);
        logger.info("{}得到的时间字符串为:{}",Thread.currentThread().getId(),dateStr);
        return dateStr;
    }

最后是调用代码

 public static void main(String[] args) throws InterruptedException {

        ExecutorService threadPool = Executors.newFixedThreadPool(100);

        for (int i = 0; i < 100; i++) {
            int finalI = i;
            //线程池中的线程
            threadPool.submit(()->{
                new MyThreadLocalDemo3().caclData(finalI);

            });
        }

        threadPool.shutdown();

    }

接下来我们就一步步从源码角度开始分析,首先我们的在线程池中的线程执行new MyThreadLocalDemo3().caclData(finalI);,此时代码走到了caclData方法,执行threadLocal.get()获取日期格式化对象。

我们查看这个get方法,可以看到ThreadLocal会从当前线程中取出一个map,然后从这个map中获取到SimpleDateFormat 。
当然如果这个map还没有创建就会通过setInitialValue完成map的创建,并调用initialValue将SimpleDateFormat 对象存到当前线程的map中。而这个initialValue我们已经在声明ThreadLocal变量时用withInitial做好了预埋。这也就意味着只要get发现map为空就会执行我们的initialValue方法完成共享变量初始化。

public T get() {
		//获取当前线程
        Thread t = Thread.currentThread();
        //拿到当前线程中的map
        ThreadLocalMap map = getMap(t);
        //如果map不为空则取用当前这个ThreadLocal作为key取出值,否则通过setInitialValue完成ThreadLocal初始化
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }


private T setInitialValue() {
		//执行initialValue为当前线程创建变量value,在这里也就是我们要用的SimpleDateFormat 
        T value = initialValue();
        //获取当前线程map,有则直接以ThreadLocal为key将SimpleDateFormat 设置进去,若没有先创建再设置
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
//返回SimpleDateFormat 
        return value;
    }

在这里插入图片描述

ThreadLocalMap有什么特点?和HashMap有什么区别

从上文我们可知ThreadLocal是通过将共享变量存到线程的map中确保线程安全,那么这个map是什么样的?它和hashMap有什么区别?

我们通过源码查看到这个map为ThreadLocalMap,它是由一个个Entry 构成的数组

 private Entry[] table;

并且每个Entry 的key是弱引用,这就意味着当触发GC时,Entry 的key也就是ThreadLocal就会被回收。

 static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

     
       

而这个map和hashma的不同点,我们也可以在resize方法中得知。当它发现要设置的值要存放的索引位置有值时,就会继续往后走,直到找到不为空的位置将值存到数组中,这也就是我们常说的线性探测法。

 private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                    //计算e要存放的索引位置
                        int h = k.threadLocalHashCode & (newLen - 1);
                        //线性探测赋值
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

ThreadLocal使用注意事项

内存泄漏问题

我们有下面这样一段web代码,每次请求test0就会像线程池中的线程存一个4M的byte数组

RestController
public class TestController {
    final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(100, 100, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());// 创建线程池,通过线程池,保证创建的线程存活

    final static ThreadLocal<Byte[]> localVariable = new ThreadLocal<Byte[]>();// 声明本地变量

    @RequestMapping(value = "/test0")
    public String test0(HttpServletRequest request) {
        poolExecutor.execute(() -> {
            Byte[] c = new Byte[4* 1024* 1024];
            localVariable.set(c);// 为线程添加变量

        });
        return "success";
    }

   
}


我们将这个代码打成jar包部署到服务器上并启动

java -jar -Xms100m -Xmx100m # 调整堆内存大小
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof  # 表示发生OOM时输出日志文件,指定path为/tmp/heapdump.hprof
-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/tmp/heapTest.log # 打印日志、gc时间以及指定gc日志的路径
demo-0.0.1-SNAPSHOT.jar

只需频繁调用几次,就会输出OutOfMemoryError

Exception in thread "pool-1-thread-5" java.lang.OutOfMemoryError: Java heap space
        at com.example.jstackTest.TestController.lambda$test0$0(TestController.java:25)
        at com.example.jstackTest.TestController$$Lambda$582/394910033.run(Unknown Source)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

问题的根本原因是我们没有及时回收Thread从ThreadLocal中得到的变量副本。因为我们的使用的线程是来自线程池中,所以线程使用结束后并不会被销毁,这就使得ThreadLocal中的变量副本会一直存储与线程池中的线程中,导致OOM。

在这里插入图片描述

可能你会问了,不是说Java有GC回收机制嘛?为什么还会出现Thread中的ThreadLocalMap的value不会被回收呢?

我们上文提到ThreadLocal得到值,都会以ThreadLocal为key,ThreadLocal的initialValue方法得到的value作为值生成一个entry对象,存到当前线程的ThreadLocalMap中。
而我们的Entry的key是一个弱引用,这就使得一旦堆区空间不足就会触发CC时,这个key就会被回收,而我们这个key所对应的value仍然被线程池中的线程的强引用引用着,所以就迟迟无法回收,随着每个线程都出现这种情况导致OOM。

在这里插入图片描述

所以我们每个线程使用完ThreadLocal之后,一定要使用remove方法清楚ThreadLocalMap中的value。

localVariable.remove()

从源码中可以看到remove方法会遍历当前线程map然后将强引用之间的联系切断,确保下次GC可以回收掉可以无用对象。

private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

空指针问题

使用ThreadLocal存放包装类的时候也需要注意添加初始化方法,否则在拆箱时可能会出现空指针问题。

 private  static ThreadLocal<Long> threadLocal = new ThreadLocal<>();


    public static void main(String[] args) {
        Long num = threadLocal.get();
        long sum=1+num;

    }

输出错误:

Exception in thread "main" java.lang.NullPointerException
	at com.guide.base.MyThreadLocalNpe.main(MyThreadLocalNpe.java:11)

解决方式

 private  static ThreadLocal<Long> threadLocal = ThreadLocal.withInitial(()->new Long(0));

线程重用问题

这个问题和OOM问题类似,在线程池中服用同一个线程未及时清理,导致下一次HTTP请求时得到上一次ThreadLocal存储的结果。


ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> null);


 * 线程池中使用threadLocal示例
     *
     * @param accountCode
     * @return
     */
    @GetMapping("/account/getAccountByCode/{accountCode}")
    @SentinelResource(value = "getAccountByCode")
    ResultData<Map<String, Object>> getAccountByCode(@PathVariable(value = "accountCode") String accountCode) throws InterruptedException {
        Map<String, Object> result = new HashMap<>();
        
        CountDownLatch countDownLatch = new CountDownLatch(1);
        
        
        threadPool.submit(() -> {

            String before = Thread.currentThread().getName() + ":" + threadLocal.get();
            log.info("before:" + before);
            result.put("before", before);

            log.info("调用getByCode,请求参数:{}", accountCode);
            QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("account_code", accountCode);
            Account account = accountService.getOne(queryWrapper);

            String after = Thread.currentThread().getName() + ":" + account.getAccountName();
            result.put("after", account.getAccountName());
            log.info("after:" + after);

            threadLocal.set(account.getAccountName());
            
            //完成计算后,使用countDown按下倒计时门闩,通知主线程可以执行后续步骤
            countDownLatch.countDown();

        });

        //等待上述线程池完成
        countDownLatch.await();

        return ResultData.success(result);
    }

从输出结果可以看出,我们第二次进行HTTP请求时,threadLocal第一get获得了上一次请求的值,出现脏数据。


C:\Users\xxx>curl http://localhost:9000/account/getAccountByCode/demoData
{"status":100,"message":"操作成功","data":{"before":"pool-2-thread-1:null","after":"pool-2-thread-1:demoData"},"success":true,"timestamp":1678410699943}
C:\Users\xxx>curl http://localhost:9000/account/getAccountByCode/Zsy
{"status":100,"message":"操作成功","data":{"before":"pool-2-thread-1:demoData","after":"pool-2-thread-1:zsy"},"success":true,"timestamp":1678410707473}

解决方法也很简单,手动添加一个threadLocal的remove方法即可

@GetMapping("/account/getAccountByCode/{accountCode}")
    @SentinelResource(value = "getAccountByCode")
    ResultData<Map<String, Object>> getAccountByCode(@PathVariable(value = "accountCode") String accountCode) throws InterruptedException {
        Map<String, Object> result = new HashMap<>();

        CountDownLatch countDownLatch = new CountDownLatch(1);

        try {
            threadPool.submit(() -> {

                String before = Thread.currentThread().getName() + ":" + threadLocal.get();
                log.info("before:" + before);
                result.put("before", before);

                log.info("调用getByCode,请求参数:{}", accountCode);
                QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
                queryWrapper.eq("account_code", accountCode);
                Account account = accountService.getOne(queryWrapper);

                String after = Thread.currentThread().getName() + ":" + account.getAccountName();
                result.put("after", after);
                log.info("after:" + after);

                threadLocal.set(account.getAccountName());

                //完成计算后,使用countDown按下倒计时门闩,通知主线程可以执行后续步骤
                countDownLatch.countDown();

            });
        } finally {
            threadLocal.remove();
        }


        //等待上述线程池完成
        countDownLatch.await();

        return ResultData.success(result);
    }

ThreadLocal的不可继承性

通过代码证明ThreadLocal的不可继承性

如下代码所示,ThreadLocal子线程无法拿到主线程维护的内部变量

/**
 * ThreadLocal 不具备可继承性
 */
public class ThreadLocalInheritTest {
    private static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    private static Logger logger = LoggerFactory.getLogger(ThreadLocalInheritTest.class);

    public static void main(String[] args) {
        THREAD_LOCAL.set("mainVal");
        logger.info("主线程的值为: " + THREAD_LOCAL.get());

        new Thread(() -> {
            try {
                //睡眠3s确保上述逻辑运行
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            logger.info("子线程获取THREAD_LOCAL的值为:[{}]", THREAD_LOCAL.get());
        }).start();
    }

}

使用InheritableThreadLocal实现主线程内部变量继承

如下所示,我们将THREAD_LOCAL 改为InheritableThreadLocal类即可解决问题。

/**
 * ThreadLocal 不具备可继承性
 */
public class ThreadLocalInheritTest {

    private static ThreadLocal<String> THREAD_LOCAL = new InheritableThreadLocal<>();

    private static Logger logger = LoggerFactory.getLogger(ThreadLocalInheritTest.class);

    public static void main(String[] args) {
        THREAD_LOCAL.set("mainVal");
        logger.info("主线程的值为: " + THREAD_LOCAL.get());

        new Thread(() -> {
            try {
                //睡眠3s确保上述逻辑运行
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            logger.info("子线程获取THREAD_LOCAL的值为:[{}]", THREAD_LOCAL.get());
        }).start();
    }

}

基于源码剖析原因

因为 ThreadLocal会将变量存储在线程的 ThreadLocalMap中,所以我们先看看InheritableThreadLocal的getMap方法,从而定位到了inheritableThreadLocals

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

然后我们到Thread类去定位这个变量的使用之处,所以我们在创建线程的地方打了个断点

在这里插入图片描述

从而定位到这段init代码,它会获取主线程的ThreadLocalMap并将主线程ThreadLocalMap中的值存到子线程的ThreadLocalMap中。

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

		//获取当前线程的主线程
        Thread parent = currentThread();
       
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
            //将主线程的map的值存到子线程中
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

createInheritedMap内部就会调用ThreadLocalMap方法将主线程的ThreadLocalMap的值存到子线程的ThreadLocalMap中。

private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

ThreadLocal在Spring中的运用

DateTimeContextHolder

	private static final ThreadLocal<DateTimeContext> dateTimeContextHolder =
			new NamedThreadLocal<>("DateTimeContext");
使用示例

该工具类和simpledateformate差不多,使用示例如下所示,是spring封装的,使用起来也很方便

public class DateTimeContextHolderTest {


    protected static final Logger logger = LoggerFactory.getLogger(DateTimeContextHolderTest.class);

    private final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    private Set<String> set = new ConcurrentHashSet<String>();

    @Test
    public void test_withLocale_same() throws Exception {
        ExecutorService threadPool = Executors.newFixedThreadPool(30);

        for (int i = 0; i < 30; i++) {
            int finalI = i;
            threadPool.execute(() -> {
                LocalDate currentdate = LocalDate.now();
                int year = currentdate.getYear();
                int month = currentdate.getMonthValue();
                int day = 1 + finalI;
                LocalDate date = LocalDate.of(year, month, day);

                DateTimeFormatter fmt = DateTimeContextHolder.getFormatter(formatter, null);
                String text = date.format(fmt);
                set.add(text);
                logger.info("转换后的时间为" + text);
            });
        }

        threadPool.shutdown();
        while (!threadPool.isTerminated()) {

        }

        logger.info("查看去重后的数量"+set.size());


    }
}

为什么JDK建议将ThreadLocal设置为static

我们都知道使用static是属于类,存在于方法区中,即修饰的变量是全局共享的,这意味着当前ThreadLocal在通过static之后,即所有的实例对象都共享一个ThreadLocal。从而避免重复创建TSO(Thread Specific Object)ThreadLocal所关联的对象的创建的开销。
这样的做法使得即使出现内存泄漏也是O(1)级别的内存泄露,场景如下:

  1. 假设使用线程非线程池模式,即线程结束后threadLocalMap就会被回收,这种情况下也只有在threadLocal第一次调用get到线程销毁之间的时间段存在内存泄漏的情况。
  2. 如果使用的是全局线程池,因为线程池的线程并不会被回收,所以threadLocalMap中的entry一直存在于堆内存中,但由于该ThreadLocal属于全局共享,所以大量线程进行操作时一定概率触发expungeStaleEntry清除过期对象,一定程度上避免了内存泄漏的情况。
  3. 极端情况下,如果threadLocal创建之后只有线程池中的一个线程get或初始化后完全没有线程再去使用,这就会导致threadLocalMap存在强引用而导致无法被回收,O(1)级别的内存泄漏由此诞生。

对应的实例变量的ThreadLocal的O(n)内存泄漏,这就不必多说。

小结

  1. ThreadLocal通过在将共享变量拷贝一份到每个线程内部的ThreadLocalMap保证线程安全。
  2. ThreadLocal使用完成后记得使用remove方法手动清理线程中的ThreadLocalMap过期对象,避免OOM和一些业务上的错误。
  3. ThreadLocal是不可被继承了,如果想使用主线的的ThreadLocal,就必须使用InheritableThreadLocal

更多

这篇文章并不代表我对于ThreadLocal的理解止步于此,笔者也会在后续的文章中不断进行迭代补充,如果你想实时收到这些文章的更新可以通过下方二维码关注一下笔者公众号和笔者保持交流:

在这里插入图片描述

参考资料

Java 并发 - ThreadLocal详解:https://www.pdai.tech/md/java/thread/java-thread-x-threadlocal.html#java-开发手册中推荐的-threadlocal

面试:为了进阿里,死磕了ThreadLocal内存泄露原因:https://www.cnblogs.com/Ccwwlx/p/13581004.html

ThreadLocal出现OOM内存溢出的场景和原理分析:https://www.cnblogs.com/jobbible/p/13364292.html#:~:text=取消注释:threadLocal.remove ();,结果不会出现OOM,可以看出堆内存的变化呈现锯齿状,证明每一次remove ()之后,ThreadLocal的内存释放掉了!

将ThreadLocal变量设置为private static的好处是啥? - Viscent大千的回答 - 知乎
:https://www.zhihu.com/question/35250439/answer/101676937

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/290055.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

输出回文数-第11届蓝桥杯选拔赛Python真题精选

[导读]&#xff1a;超平老师的Scratch蓝桥杯真题解读系列在推出之后&#xff0c;受到了广大老师和家长的好评&#xff0c;非常感谢各位的认可和厚爱。作为回馈&#xff0c;超平老师计划推出《Python蓝桥杯真题解析100讲》&#xff0c;这是解读系列的第23讲。 输出回文数&#…

数据分析概述

数据分析概述 在当今数字化的时代&#xff0c;数据已经成为我们周围不可忽视的存在。从商业领域到医疗行业&#xff0c;从科学研究到政府治理&#xff0c;数据的涌现为我们提供了前所未有的信息资源。然而&#xff0c;要从这个庞大的信息海洋中获取有意义的见解并做出明智的决策…

OpenCV-14图片的四则运算和图片的融合

一、图片的四则运算 1. 加法运算 通过使用API add来执行图像的加法运算 cv2.add&#xff08;src1&#xff0c; src2&#xff09;需要再其中传入两张图片。 图片就是矩阵&#xff0c;图片的加法运算就是矩阵的加法运算。 因此加法运算中要求两张图的shape必须是相同的。 首…

2023我的编程之旅-地质人的山和水

引言 大家好&#xff0c;我是搞地质的。外行人有的说我们游山玩水&#xff0c;有的说我们灰头土脸&#xff0c;也有的说我们不是科学。 而我说&#xff0c;这是一门穷极一生青春&#xff0c;值得奉献的行业。这是一门贴近民生&#xff0c;又拥抱自然的学科。他的真理性在于探…

excel 插件:Rainbow Analyst Crack

一个插件中包含四种 EXCEL 审核工具检测并修复隐藏的电子表格错误 不要满足于更少&#xff0c;四种领先的电子表格审计工具合二为一 Rainbow Analyst&#xff08;因其对颜色编码的独特强大使用而得名&#xff09;结合了世界级电子表格审核功能的多个领域&#xff1a; Excel™ …

【Spring实战】14 Web表单校验

文章目录 1. 依赖2. 实体类3. 控制器4. 页面5. 启动6. 验证1&#xff09;访问页面2&#xff09;后台校验 7. 优点8. 代码详细总结 在 Web 应用中&#xff0c;表单是用户与后端交互的主要界面之一。为了确保数据的有效性和一致性&#xff0c;我们经常需要对提交的表单数据进行验…

一文教会git如何配置多SSH Key(Github+Gitlab)

一文教会git如何配置多SSH Key&#xff08;GithubGitlab&#xff09; 对于工作了的伙伴来说&#xff0c;大家经常会配置多个SSH Key&#xff1a; SSH拉取的仓库不会反复的要求输入github用户名密码来验证自己的身份需要将公司的Gitlab和个人的Github区分开 我这里以Mac电脑为例…

Debezium发布历史46

原文地址&#xff1a; https://debezium.io/blog/2019/02/05/debezium-0-9-0-final-released/ 欢迎关注留言&#xff0c;我是收集整理小能手&#xff0c;工具翻译&#xff0c;仅供参考&#xff0c;笔芯笔芯. Debezium 0.9.0.Final 发布 2019 年 2 月 5 日 作者&#xff1a; G…

matplotlib颜色合集——各种常见简单图形(上)

一、.颜色配图表 图文颜色引用&#xff1a;建站教程 - FINTHON 二、折线图 jupyter notebook 简介plot() 2.1折线图参数 plt.plot(*args, scalexTrue, scaleyTrue, dataNone, **kwargs) 2.2、参数介绍 x&#xff1a;X轴数据&#xff0c;列表或数组&#xff0c;可选。 y&a…

没有Kubernetes也可以玩转Dapr?

一、NameResolution组件 虽然Dapr提供了一系列的编程模型&#xff0c;比如服务调用、发布订阅和Actor模型等&#xff0c;被广泛应用的应该还是服务调用。我们知道微服务环境下的服务调用需要解决服务注册与发现、负载均衡、弹性伸缩等问题&#xff0c;其实Dapr在这方面什么都没…

【数据结构】二叉树(一)——树和二叉树的概念及结构

前言: 本篇博客主要了解什么是树&#xff0c;什么是二叉树&#xff0c;以及他们的概念和结构。 文章目录 一、树的概念及结构1.1 树的基本概念1.2 树的相关特征1.3 树的实现 二、二叉树的概念及性质2.1 二叉树的概念2.2 二叉树的性质 一、树的概念及结构 1.1 树的基本概念 树&…

Z-score 因子的深入思考

最新&#xff08;2024 年 1 月&#xff09;出版的 SC 技术分析&#xff08;Techical Analysis of Stock & Commodities&#xff09;的第 4 条文章给到了 Z-score&#xff0c;原文标题为《Z-score: How to use it in Trading》。今天的笔记&#xff0c;就借此机会&#xff0…

C++线程池的原理(画图)及简单实现+例子(加深理解)

1.为什么线程池会出现&#xff0c;解决什么问题&#xff1f; C线程池&#xff08;ThreadPool&#xff09;的出现主要是为了解决以下几个问题&#xff1a; 1.性能&#xff1a;创建和销毁线程都是相对昂贵的操作&#xff0c;特别是在高并发场景下&#xff0c;频繁地创建和销毁线…

ubuntu18.04安装MySQL

1.安装mysql服务器端 sudo apt-get -y install mysql-server&#xff08;18.04/20.04不会提示输入密码&#xff0c;默认是没有密码&#xff09; 2.安装mysql客户端 sudo apt-get -y install mysql-client3.安装mysql模块 sudo apt-get -y install libmysqlclient-dev4.验证是…

data.TensorDataset解析

data.TensorDataset 是 PyTorch 中的一个类&#xff0c;用于创建一个包含多个张量的数据集。这个类的主要作用是将输入的张量组合成一个数据集&#xff0c;使得在训练过程中可以方便地进行数据加载和迭代。 具体来说&#xff0c;TensorDataset 接受一系列的张量作为输入参数&a…

字符集字符编码

字符集 字符&#xff08;Character&#xff09;是各种文字和符号的总称&#xff0c;包括各国家文字、标点符号、图形符号、数字等。而字符集&#xff08;Character set&#xff09;则是多个字符的集合。 简单的说&#xff0c;字符集就规定了某个文字对应的二进制数字存放方式…

springboot整合springbatch批处理

springboot整合springbatch实现批处理 简介项目搭建步骤 简介 项目搭建 参考博客【场景实战】Spring Boot Spring Batch 实现批处理任务&#xff0c;保姆级教程 步骤 1.建表 建表sql CREATE TABLE student (id int NOT NULL AUTO_INCREMENT,name varchar(100) NOT NULL C…

[C#]yolov8-onnx在winform部署手势识别模型

【官方框架地址】 https://github.com/ultralytics/ultralytics.git 【算法介绍】 YOLOv8 是一个 SOTA 模型&#xff0c;它建立在以前 YOLO 版本的成功基础上&#xff0c;并引入了新的功能和改进&#xff0c;以进一步提升性能和灵活性。具体创新包括一个新的骨干网络、一个新…

牛客网面试题知识点记录-03

1.题目讲解重写后子类调用父类的方法总结&#xff1a;当子类重写了父类方法A&#xff0c;父类方法直接调用被重写的父类方法后&#xff0c;调用的是子类的重写的父类方法A。 class Test {public static void main(String[] args) {System.out.println(new B().getValue());}st…

Java的并发修改异常

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 学习必须往深处挖&…