ThreadLocal的作用、原理、存在问题以及应用场景

1. 什么是ThreadLocal以及它的作用

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果实现每个线程都有自己的专属变量该如何设置?ThreadLocal类主要就是解决让每个线程绑定自己的专属变量。
ThreadLocal主要用来存储当前上下文的变量信息,他可以保障存储进去的信息只能被当前的线程读取到,并且线程之间不会受到影响。ThreadLocal为变量在每个线程都创建了一个副本,那么每个线程可以访问自己的内部的副本变量。

2. ThreadLocal的原理

2.1 源码

我们可以看到ThreadLocal中的set()、get()方法都是调用了ThreadLocalMap中的set()、get()方法。最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。
ThreadLocalMap的map,这个map就是用来存储与这个线程绑定的变量,map的key就是ThreadLocal对象,value就是线程正在执行的任务中的某个变量的包装类Entry。

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();
}
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

3. ThreadLocal引发的新问题

3.1 内存泄露问题

ThreadLocalMap中的key是弱引用,而value是强引用。所以,如果ThreadLocal没有被外部强引用的情况下,在垃圾回收时,key会被清空掉,而value不会。ThreadLocal中就会出现key为null的Entry。假如我们不采取措施,value永远不会被GC回收,这个时候就可能会产生内存泄漏问题。

3.2 解决思路

ThreadLocalMap实现中已经考虑了这种情况,在调用set()、get()、remove()方法时会处理掉key为null的记录。使用ThreadLocal时,最好手动调用remove()方法。

4. ThreadLocal的应用场景

  1. 数据库事务:通过AOP的方式,对执行数据库事务的函数进行拦截。函数开始前,获取connection开启事务并存储在ThreadLocal中,任何用到connection的地方,从ThreadLocal中获取。函数执行完毕,提交事务释放connection。
  2. 解决线程安全的问题。比如Java7中的SimpleDateFormat不是线程安全的,可以用ThreadLocal来解决这个问题。
ackage fuxi.ThreadLocal;

import java.text.SimpleDateFormat;
import java.util.Random;

public class ThreadLocalExample implements Runnable{
    private static final ThreadLocal<SimpleDateFormat>formatter=ThreadLocal.withInitial(()->
            new SimpleDateFormat("yyyy-MM--dd :HH-mm"));
            
    public static void main(String[]args){
     ThreadLocalExample obj=new ThreadLocalExample();
     for(int i=0;i<10;i++){
         Thread t=new Thread(obj,""+i);
         try {
             Thread.sleep(100);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         t.start();
     }
    }
    @Override
    public void run() {
        System.out.println("Thread Name="+Thread.currentThread().getName()+" "+"default formatter="+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        formatter.set(new SimpleDateFormat());
        System.out.println("Thread Name="+Thread.currentThread().getName()+" "+"formatter="+formatter.get().toPattern());

    }
}
  1. 用户登录信息等:用户登录信息好多个方法上都要用到,给每个方法都添加一个User非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User对象就传不进去了。而ThreadLocal可以在一个线程中传递同一个对象。
package fuxi.ThreadLocal;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[]args){
        ExecutorService es= Executors.newFixedThreadPool(3);
        String[] users = new String[] { "Bob", "Alice", "Tim", "Mike", "Lily", "Jack", "Bush" };
        for (String user : users) {
            es.submit(new Task(user));
        }
        es.shutdown();
    }
}
class Task implements Runnable{
     private final String name;
     
    public Task(String name){
        this.name=name;
    }
    @Override
    public void run() {
        try(UserContext ctx=new UserContext(this.name)){
            new Task1().process();
            new Task2().process();
            new Task3().process();
        }
    }
}
class Task1{
    public void process(){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("[%s] check user %s...\n", Thread.currentThread().getName(), UserContext.getCurrentUser());
    }
}
class Task2{
    public void process(){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("[%s] check user %s...\n", Thread.currentThread().getName(), UserContext.getCurrentUser());

    }
}
class Task3{
    public void process(){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("[%s] check user %s...\n", Thread.currentThread().getName(), UserContext.getCurrentUser());

    }
}
class UserContext implements AutoCloseable{
    private static final ThreadLocal<String> userThreadLocal = new ThreadLocal<>();
    public UserContext(String name){
        userThreadLocal.set(name);
        System.out.printf("[%s] init user %s...\n", Thread.currentThread().getName(), UserContext.getCurrentUser());
    }
    public static String getCurrentUser(){
        return userThreadLocal.get();
    }

    @Override
    public void close() {
        System.out.printf("[%s] cleanup for user %s...\n", Thread.currentThread().getName(),
                UserContext.getCurrentUser());
        userThreadLocal.remove();
    }
}

5. 使用ThreadLocal应该注意什么?

在使用ThreadLocal对象,尽量使用static,不然会使线程的ThreadLocalMap产生太多Entry,从而造成内存泄露。

参考资料:

https://www.iteye.com/blog/liudeh-009-1934768
https://www.liaoxuefeng.com/wiki/1252599548343744/1306581251653666

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值