线程私有:ThreadLocal

不知道大家有没有接触过ThreadLocal这个类,我还清楚的记得,自己当初第一次使用到这个类是缓存用户信息。刚开始学习java时,那个时候并没有想到什么分布式啊,单点登录(SSO),就使用ThreadLocal来保存用户一些全局信息,获取非常方便。那时,我只知道它是线程隔离的,非常好用,渐渐的,当它的身影出现越多的时候,对它的了解也就加深了一点,现在就让我们来领略一下ThreadLocal的魅力吧。

初始ThreadLocal

从类名来看,可以理解为线程本地化变量,那么它肯定就是线程安全的。在我们之前认识中,如果我们需要获得一个线程安全类,要么使用同步操作,要么就是加锁,或者使用更加高级的CAS操作。在这里,我想告诉大家,我们可以在不使用它们的情况下,获得一个线程安全的类,不信?大家都知道SimpleDateFormat是一个线程不安全的类,如果谁在并发环境中共享它,我相信大家都会鄙视的。咳咳,我当初就是被鄙视的一员,当然现在不会犯这么低级的错误了。SimpleDateFormat错误的用法:

public class SimpleDateFormatDemo {
    private final static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 50; i++) {
            int j = i;
            executorService.execute(() -> {
                Date parse = null;
                try {
                    parse = SimpleDateFormatDemo.format.parse("2019-01-" + (j % 30));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
                System.out.println(parse);
            });
        }
        executorService.shutdown();
    }
}

大家别笑(^_^),当初我就写过这样的代码,虽然和此代码不同,但是会引起的问题是一样的。大家运行这段代码,应该都会遇到类似的错误:

java.lang.NumberFormatException: For input string: ""
java.lang.NumberFormatException: For input string: "2424E"
java.lang.NumberFormatException: multiple points

这是因为SimpeDateFormat不是线程安全的,当我们在多个线程共享此变量时,各个线程读取到不一致内部状态。大家试想一下,多个线程同时使用SimpleDateFormat,假如某个线程刚设置了当前所要解析的日期,时间片用完暂停执行,另外一个线程开始执行,也设置了要解析的日期并暂停,前一个线程继续执行就会读取到不一致的结果,综上所述,SimpleDateFormat在多线程下工作是不友好的。

那么,应该怎么办呢?我们首先会想到的既然它不是线程安全的,那么我们就不共享它,在每次使用它的时候都new一个新的对象出来,这样不就可以了。大家试想一下,SimpleDateFormat是使用非常频繁的一个类,若我们每次都去创建,对内存和GC都不友好,此方法不可行。根据传统线程安全的做法,要想一个线程不安全的对象或者方法变成线程安全的,无非就是采取同步操作或者加锁,但是我们都知道采用这些方式会降低系统并发,传统方法是采用对象共享,串行化方式运行,是以时间换空间的思想,并且它的实现方式复杂,不易于理解。现在,有一种全新的方式,采用ThreadLocal,它是线程本地化变量,线程独享,多线程之间维护各自变量互不影响。ThreadLocal采用空间换时间的方式,对象独享化,有利于提高系统并发。SimpleDateFormat在多线程下安全使用:

public final class ThreadLocalDemo {
    private static ThreadLocal<SimpleDateFormat> threadLocal=new ThreadLocal<>();

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 50; i++) {
            Runnable runnable = () -> {
                if(threadLocal.get()==null){
                    System.out.println(Thread.currentThread().getId()+"初始化SimpleDateFormat");
                    threadLocal.set(new SimpleDateFormat("yyyy-MM-dd"));
                }
                try {
                    Date parse = threadLocal.get().parse("2019-01-07");
                    System.out.println(parse);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            };
            executorService.execute(runnable);
        }
        executorService.shutdown();
    }
}

这里,我们使用ThreadLocal包装SimpelDateFormat,然后在多线程中使用它,我们来看看结果如何。执行如下(截取部分输出):

16初始化SimpleDateFormat
17初始化SimpleDateFormat
15初始化SimpleDateFormat
14初始化SimpleDateFormat
20初始化SimpleDateFormat
21初始化SimpleDateFormat
18初始化SimpleDateFormat
22初始化SimpleDateFormat
23初始化SimpleDateFormat
19初始化SimpleDateFormat
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019
Mon Jan 07 00:00:00 SGT 2019

大家可以发现,我们线程池中定义了10个线程,循环50次执行,SimpelDateFormat只初始化10次,这说明什么?这说明10个线程间使用的对象不是同一个,它们是线程私有的,互不影响,并且下一次线程执行的时候,发现对象已经初始化之后会直接获取执行。它是怎么做到的呢?请看下面这三个方法:

public void set(T value);//该方法是ThreadLocal用来保存局部变量
public T get();//该方法用来获取ThreadLocal保存的局部变量
public void remove();//该方法用来移除ThreadLocal保存的局部变量

下面我们来分别分析这三个方法:

set()

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //根据当前线程获取ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //map不为空,当前ThreadLocal作为key,插入value到map中
        map.set(this, value);
    else
        //否则创建map并把对象保存
        createMap(t, value);
}

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

关于threadLocals,Thread.java中定义如下:

ThreadLocal.ThreadLocalMap threadLocals = null;

set()方法,首先获取当前线程,然后根据当前线程获取ThreadLocalMap(与平时使用的Map没有关系,它是ThreadLocal的静态内部类)数据存储结构。如果map不为空,当前ThreadLocal对象作为key,把value插入到map中;否则根据当前线程创建ThreadLocalMap,然后把value保存进去。其中ThreadLocalMap是Thread的成员变量,说明ThreadLocalMap是线程独享的,只能根据当前线程去获取。

get()

public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //根据当前线程获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //根据当前ThreadLocal对象获取Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            //获取结果返回
            T result = (T)e.value;
            return result;
        }
    }
    //若map为空,初始化map,插入null值
    return setInitialValue();
}
//数据保存在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;
    }
}

get()方法用户获取线程局部变量,首先根据当前线程获取ThreadLocalMap,若map不为空,根据ThreadLocal对象获取Entry对象,然后获取value,若map为空则初始化map并赋null值。若未调用set()方法,先调用get(),返回null。这里的Entry被定义为弱引用,弱引用就是比强引用弱很多的引用,当jvm进行垃圾回收时,发现弱引用,会立即进行回收。

remove()

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
    //根据当前ThreadLocal对象移除数据
        m.remove(this);
}

注意:ThreadLocalMap作为Thread的成员变量,当线程使用完未被销毁,ThreadLocalMap中保存的局部变量还是会一直存在,这就会引发内存泄漏。特别是在和线程池结合使用时,假如你现在定义一个线程池,为线程存储局部变量,当线程使用结束,并未把线程进行销毁而是直接归还到池中,内存泄漏就无可避免,所以当使用完毕最好使用remove()清除局部变量或者使用threadLocal=null。

总结

ThreadLocal和传统做法不一样的地方就在于,它是以空间换时间的方式,对象独享,实现线程安全。它不足的地方就在于会创建变量副本,增大内存消耗,但它能消除同步和锁产生的性能消耗,提高系统并发,所以它还是有自己的独到之处。

在这里多说一句,今天在公司关于SimpleDateFormat遇到这样一个问题,上游系统传到下游系统2019-02-30的字符串日期,然后使用SimpleDateFormat解析变成了2019-03-02号,导致还款计划错误。本身2019-02-30这个日期就是错误的,如果日期不合法,解析应该抛出异常而不是自动转换。后面发现SimpleDateFormat有这样一个方法setLenient(boolean b),当传入值为false时,进行严格校验,不合法抛出异常,为true时,则进行不严格校验并自动转换。大家若对日期有严格的校验,不妨试试这个方法。

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值