ThreadLocal


前言

本文主要介绍ThreadLocal,包括它的底层结构、源码以及注意事项(内存泄漏问题)

一、ThreadLocal案例

案例一:使用线程池打印日期信息

public class ThreadLocalNormalUsage03 {
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

	//利用SimpleDateFormat对象,返回日期信息
    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        return dateFormat.format(date);
    }

    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 ThreadLocalNormalUsage03().date(10 + finalI);
                    //打印日期信息(每个线程打印的都不一样)
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }
}

最后的结果可以发现,会出现相同的值,这说明发生了线程安全问题。因为所有线程公用了一个SimpleDateFormat对象,当发生安全问题时,导致拿到的值相同
这时候可以考虑加锁:

	synchronized (ThreadLocalNormalUsage04.class) {
        s = dateFormat.format(date);
    }

然而加锁的效率太低。

这时候就可以考虑使用ThreadLocal,利用ThreadLocal,给每个线程分配自己的dateFormat对象,既保证线程安全,又高效利用内存

public class ThreadLocalNormalUsage05 {

    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        //通过threadLocal的get方法获取到SimpleDateFormat对象
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }

    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 ThreadLocalNormalUsage05().date(10 + finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }
}

class ThreadSafeFormatter {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        //初始化方法一,重写initialValue
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };
    //方法二:利用lambda表达式
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}

案例二:全局变量

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

class Service {
    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);
        new Service3().process();
    }
}

class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用户:" + user.name);
        UserContextHolder.holder.remove();  //确保不会出现内存泄漏
    }
}

class User {
    String name;

    public User(String name) {
        this.name = name;
    }
}

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

通过ThreadLocal保存全局变量,可以避免传参的麻烦

两种场景总结


二、ThreadLocal原理

1.结构

每个线程存在一个ThreadLocalMap,每个Map里面包含多个ThreadLocal,而每个ThreadLocal都是以K-V形式存储的

2.方法

2.1 initialValue()初始化

  • 该方法会返回当前线程对应的"初始值",这是一个延迟加载的方法,只有在调用get的时候,才会触发。

  • 当线程第一次使用get方法访问变量时,将调用此方法,除非线程先调用了set方法,在这种情况下,不会为线程调用本initialValue方法。

  • 通常每个线程最多调用一次此方法,但如果已经调用了remove()后,在调用get(),则可以再次调用此方法。(remove之后,map变为null)

  • 如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。

   //初始化方法一,重写initialValue
   public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };

2.2 get()

  • 拿到当前线程对象
  • 获取map
  • 如果存在,就去获取entry,即K-V
  • 拿到entry后如果不为null,就可以拿到value

2.3 set()

public static ThreadLocal<User> holder = new ThreadLocal<>();
holder.set(user);
  • 拿到map
  • 向map里面存K-V

2.4 remove()

  • 删除掉当前的entry

3.ThreadLocalMap

  • threadLocalMap类似于是一个HashMap
  • 它在发生冲突的时候采用的是线性探测法,就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链

4.内存泄漏

  • 当某个对象不再有用,但是占用的内存却不能被回收。

ThreadLocal就存在内存泄漏的问题
在上面的ThreadLocalMap里面,最重要的就是Entry对象,它包含key-value,可以看到整个Entry对象继承WeakReference是弱引用(当某个对象只被弱引用关联,它就可以被回收),然而,value=v属于强引用

  • 正常情况下,当线程停止,它里面的value就会被垃圾回收,那么就不存在强引用了
  • 但是当线程不终止,那么key就不能被回收,就存在
    Thread——>ThreadLocalMap——>Entry(key为null)——>Value

JDK已经考虑到了这样的问题,在set,remove,rehash这些方法中会扫描key为null的Entry,并把value设置为null,这样就可以被回收

但是,若这些方法都没有被调用,线程又不停止,那么就会出现内存泄漏
在使用完ThreadLocal之后(业务逻辑中不需要再使用),主动调用remove()方法,避免内存泄漏。


总结

复习重点:initialize()、get()、内存泄漏

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值