ThreadLocal

  • 什么时候需要使用ThreadLocal
    1. 每个线程需要一个独享的对象,选择initialValue保存对象(通常是工具类,典型需要的类有SimpleDateFormat和Random)
    2. 每个线程需要保存全局变量,可以让不同方法直接调用,避免参数传递的麻烦,用set方法保存对象

在这里插入图片描述
在这里插入图片描述
第一种

/**
 * 利用threadLocal给每个线程分配自己的dataformat对象,保证了线程安全,高效利用线程。
 */
public class ThreadLocalNormalUsege05 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<1000;i++){
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsege05().date(finalI);
                    System.out.println(date);

                }
            });
        }
        threadPool.shutdown();

    }
    public String date(int seconds){
        //参数的单位是毫秒,从1970.1.1 00:00:00开始 
        Date date = new Date(seconds * 1000);
        SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.simpleDateFormatThreadLocal2.get();
        return simpleDateFormat.format(date);
    }
}

class ThreadSafeFormatter{
    public static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };
    //lamda表达式写法
    public static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal2 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}

第二种

/**
 * 演示threadlocal用法2:避免参数的层层传递
 */
public class ThreadLocalNormalUsege06 {

    public static void main(String[] args) {
        new Service1().process();
    }
}


class Service1 {
    public void process() {
        User user = new User("超哥");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2拿到用户名:" + user.name);
//        UserContextHolder.holder.remove();
//        User user1 = new User("王姐");
//        UserContextHolder.holder.set(user1);
        new Service3().process();
    }
}

class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用户名:" + user.name);
        UserContextHolder.holder.remove();
    }
}

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

}

class User {
    String name;

    public User(String name) {
        this.name = name;
    }
}
  • 场景一:initialValue
    在ThreadLocal第一次get的时候把对象初始化出来,对象初始化时机可以由我们控制。
  • 场景二:set
    如果需要保存到ThreadLocal里的对象生成时机不由我们随意控制,例如拦截器生成的用户数据,用ThreadLocal.set将数据直接存放到ThreadLocal中去,以便后续使用。
  • 使用ThreadLocal的好处
    1. 线程安全
    2. 不需要加锁,提高效率
    3. 更高效的利用内存,节省开销:相比每个任务新建一个SimpleDateFormat,上面用ThreadLocal更节省内存和开销
    4. 免去传参的繁琐,无论场景一的工具类或者场景二的用户信息,都可以在任意地方通过ThreadLocal拿到,再也无需每次传相同的参数,ThreadLocal使得代码耦合性更低更优雅。
  • Thread、ThreadLocal和ThreadLocalMap关系

在这里插入图片描述

  • 主要方法介绍
  1. initialValue()
    1. 该方法会返回对应线程的“初始值”,这是一个延迟加载的方法,只有在调用get()的时候才会触发。
    2. 当线程第一次调用get()方法访问变量时,将调用此方法,除非线程先前调用了set()方法,在这种情况下,不会为线程调用本initialValue()。
    3. 通常线程会调用一次此方法,但是如果调用了remove方法,再调用get方法,则可以再次调用此方法。
    4. 如果不重写此方法,这个方法会返回null。一般使用匿名内部内的方法来重写此方法,以便在后续使用中可以初始化副本对象。
  2. get()
    1. get()方法是先取出当前线程的ThreadLocalMap,然后调用Map.getEntry方法,吧ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的Value。
    2. 注意,这个map以及map的key和value是保存在线程中的,而不是保存在ThreadLocal中。
  • ThreadLocalMap类
    • ThreadLocalMap类是每个线程Thread类里面的变量,里面最重要的是一个键值数组Entry[] table,可以认为是一个map,键值对:
    1. 键:这个ThreadLocal
    2. 值:实际需要的成员变量,比如user或者SimpleDateFormat对象
  • 内存泄露
    某个对象不再有用,但是占用的内存却不能被回收,
    Value的泄露:
    • ThreadLocalMap的每个Entry都是一个对key的弱引用,同时每个Entry都包含一个对Value的强引用。
    • 正常情况下,当线程终止,保存在ThreadLocal里的Value会被垃圾回收,因为没有任何强引用了。
    • 但是,如果线程不终止(比如线程需要保持很久),那么key对的Value就不能被回收,因为有以下调用链:
      Thread -> ThreadLocalMap -> Entry(key为null) -> Value
    • 因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能出现OOM。
    • JDK已经考虑到这个问题,所以set、remove、rehash方法中会扫描key为null的Entry,并把对应的Value设置成null,这样value对象就能被回收了。
    • 但是如果一个线程ThreadLocal不被使用,那么实际上set、remove、rehash就不会再被调用,如果同时线程又不停止,那么调用链就一直存在,就导致了value的内存泄露。
  • 避免内存泄露
    • (阿里规定)调用remove方法,就会删除对应的Entry对象,可以避免内存泄露,所以使用完ThreadLocal之后,应该调用remove方法。
  • ThreadLocal注意点
    1. 如果可以不使用ThreadLocal就能解决问题就不要强行使用。
    2. 优先使用框架的支持,而不是自己创造,例如在spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,防止忘记调用remove()方法,造成内存泄露。
    3. 在get()之前,必须set(),否则可能会报空指针异常。
    4. ThreadLocal不能塞入共享对象,比如static对象,因为这样ThreadLocal.get()取得的还是这个共享对象本身,还是会发生并发访问问题。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值