一 问题抛出
SimpleDateFormat是非线程安全的,在多线程情况下会遇见问题:
public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); List<String> dateStrList = Lists.newArrayList( "2018-04-01 10:00:01", "2018-04-02 11:00:02", "2018-04-03 12:00:03", "2018-04-04 13:00:04", "2018-04-05 14:00:05" ); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); for (String str : dateStrList) { executorService.execute(() -> {//多线程共享同一个simpleDateFormat对象 try { simpleDateFormat.parse(str); TimeUnit.SECONDS.sleep(1); } catch (Exception e) { e.printStackTrace(); } }); } }
上述代码在多线程下可能会抛出异常。
解决方案1,使用局部变量:
public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); List<String> dateStrList = Lists.newArrayList( "2018-04-01 10:00:01", "2018-04-02 11:00:02", "2018-04-03 12:00:03", "2018-04-04 13:00:04", "2018-04-05 14:00:05" ); for (String str : dateStrList) { executorService.execute(() -> { try { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); simpleDateFormat.parse(str); TimeUnit.SECONDS.sleep(1); } catch (Exception e) { e.printStackTrace(); } }); } }
这样虽然解决的线程安全问题,但是每次执行都需要创建一个SimpleDateFormat对象,性能不是很好。
解决方案二,使用线程局部变量:
/** * 使用ThreadLocal以空间换时间解决SimpleDateFormat线程安全问题 */ public class DateUtil { private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; @SuppressWarnings("rawtypes") private static ThreadLocal threadLocal = new ThreadLocal() { protected synchronized Object initialValue() { return new SimpleDateFormat(DATE_FORMAT); } }; public static DateFormat getDateFormat() { return (DateFormat) threadLocal.get(); } public static Date parse(String textDate) throws ParseException { return getDateFormat().parse(textDate); } }
二 理解ThreadLocal
ThreadLocal,即线程本地变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。这样多个线程都可以随意更改自己线程局部的变量,不会影响到其他线程。
需要注意的是,ThreadLocal提供的只是一个浅拷贝,如果变量是一个引用类型,那么就要考虑它内部的状态是否会被改变,想要解决这个问题可以通过重写ThreadLocal的initialValue()函数来自己实现深拷贝,建议在使用ThreadLocal时一开始就重写该函数。
ThreadLocal与像synchronized这样的锁机制是不同的。首先,它们的应用场景与实现思路就不一样,锁更强调的是如何同步多个线程去正确地共享一个变量,ThreadLocal则是为了解决同一个变量如何不被多个线程共享。从性能开销的角度上来讲,如果锁机制是用时间换空间的话,那么ThreadLocal就是用空间换时间。
1、ThreadLocal提供了一种访问某个变量的特殊方式:访问到的变量属于当前线程,即保证每个线程的变量不一样,而同一个线程在任何地方拿到的变量都是一致的,这就是所谓的线程隔离。
2、如果要使用ThreadLocal,通常定义为private static类型,在我看来最好是定义为private static final类型。
ThreadLocal可以总结为一句话:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
先了解一下ThreadLocal类提供的几个方法:
public T get() { } //用来获取ThreadLocal在当前线程中保存的变量副本 public void set(T value) { } //用来设置当前线程中变量的副本 public void remove() { } //用来移除当前线程中变量的副本 protected T initialValue() { } //一个protected方法,用来返回此线程局部变量的当前线程的初始值,一般是在使用时进行重写的,它是一个延迟加载方法
1、get()方法解析
首先我们来看一下ThreadLocal类是如何为每个线程创建一个变量的副本的。先看下get方法的实现:
public T get() { //1.首先获取当前线程 Thread t = Thread.currentThread(); //2.获取当前线程的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); //3.如果map不为空,以threadlocal实例为key获取到对应Entry,然后从Entry中取出对象即可。 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this);//这里的this即ThreadLocal对象 if (e != null) return (T)e.value; } //如果map为空,也就是第一次没有调用set直接get(或者调用过set,又调用了remove)时,为其设定初始值 return setInitialValue(); }
首先是取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap,这里的入参为当前线程,返回的是当前线程中的实例变量。
然后接着下面获取到Entry<key,value>键值对,注意这里获取键值对传进去的是this即当前ThreadLocal对象,而不是当前线程t。如果获取成功,则返回value值。如果map为空,则调用setInitialValue方法初始化value。
首先看一下getMap方法中做了什么:
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
在getMap中,是调用当期线程t,返回当前线程t中的一个成员变量threadLocals,线程Thread类里持有了一个threadLocals成员变量:
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap是ThreadLocal类的一个内部类,ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键值。
因此,get()方法的主要操作是获取属于当前线程的ThreadLocalMap,如果这个map不为空,我们就以当前的ThreadLocal为键,去获取相应的Entry,Entry是ThreadLocalMap的静态内部类,它继承于弱引用,所以在get()方法里面如第10行一样调用e.value方法就可以获取实际的资源副本值。
但是如果获取到的map为空,说明属于该线程的资源副本还不存在,则需要去创建资源副本,从代码中可以看到是调用setInitialValue()方法,其定义如下:
private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread();//获取到当前线程 ThreadLocalMap map = getMap(t);//获取到当前线程的成语变量 if (map != null)//如果不为空 map.set(this, value);//设置值,这里this即当前ThreadLocal对象 else createMap(t, value);//如果map为空,则需要先初始化一个map再设置值 return value; }
第2行调用initialValue()方法初始化一个值。接下来是判断线程的ThreadLocalMap是否为空,不为空就直接设置值(键为this,值为value),为空则创建一个Map,调用方法为createMap(),其定义如下:
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
2、set()方法解析
public void set(T value) { // 获取当前线程对象 Thread t = Thread.currentThread(); // 获取当前线程本地变量Map ThreadLocalMap map = getMap(t); // map不为空 if (map != null) // 存值 map.set(this, value); else // 创建一个当前线程本地变量Map createMap(t, value); }
首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。
因此ThreadLocal为每个线程创建变量的副本的具体流程如下:
(1)首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
(2)初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
(3)然后在当前线程里面,如果要使用副本变量,就可以通过get方法在当前线程的threadLocals里面查找。
总结一下:
1)实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量;
3)在进行get之前,必须先set,否则会报空指针异常。 如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。
注意 :默认情况下 initValue(),返回 null 。线程在没有调用 set 之前,第一次调用 get 的时候, get方法会默认去调用 initValue 这个方法。所以如果没有覆写这个方法,可能导致 get 返回的是 null 。当然如果调用过 set 就不会有这种情况了。但是往往在多线程情况下我们不能保证每个线程的在调用 get 之前都调用了set ,所以最好对 initValue 进行覆写,以免导致空指针异常。
三 ThreadLocal使用的一般步骤
(1)在多线程的类(如ThreadDemo类)中,创建一个private static类型的ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。
(2)在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。
(3)在ThreadDemo类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。
7、ThreadLocal 与 synchronized 的对比
(1)ThreadLocal和synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
(2)synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
8、一句话理解ThreadLocal:向ThreadLocal里面存东西就是向它里面的Map存东西的,然后ThreadLocal把这个Map挂到当前的线程底下,这样Map就只属于这个线程了。
四 ThreadLocal中的内存泄漏
如果ThreadLocal被设置为null后,而且没有任何强引用指向它,根据垃圾回收的可达性分析算法,ThreadLocal将会被回收。这样一来,ThreadLocalMap中就会含有key为null的Entry,而且ThreadLocalMap是在Thread中的,只要线程迟迟不结束,这些无法访问到的value会形成内存泄漏。为了解决这个问题,ThreadLocalMap中的getEntry()、set()和remove()函数都会清理key为null的Entry,以下面的getEntry()函数的源码为例。
private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> var1) { int var2 = var1.threadLocalHashCode & this.table.length - 1; ThreadLocal.ThreadLocalMap.Entry var3 = this.table[var2]; return var3 != null && var3.get() == var1 ? var3 : this.getEntryAfterMiss(var1, var2, var3); } private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> var1, int var2, ThreadLocal.ThreadLocalMap.Entry var3) { ThreadLocal.ThreadLocalMap.Entry[] var4 = this.table; for(int var5 = var4.length; var3 != null; var3 = var4[var2]) { ThreadLocal var6 = (ThreadLocal)var3.get(); if (var6 == var1) { return var3; } if (var6 == null) { this.expungeStaleEntry(var2); } else { var2 = nextIndex(var2, var5); } } return null; }
在上文中我们发现了ThreadLocalMap的key是一个弱引用,那么为什么使用弱引用呢?使用强引用key与弱引用key的差别如下:
-
强引用key:ThreadLocal被设置为null,由于ThreadLocalMap持有ThreadLocal的强引用,如果不手动删除,那么ThreadLocal将不会回收,产生内存泄漏。
-
弱引用key:ThreadLocal被设置为null,由于ThreadLocalMap持有ThreadLocal的弱引用,即便不手动删除,ThreadLocal仍会被回收,ThreadLocalMap在之后调用set()、getEntry()和remove()函数时会清除所有key为null的Entry。
但要注意的是,ThreadLocalMap仅仅含有这些被动措施来补救内存泄漏问题。如果你在之后没有调用ThreadLocalMap的set()、getEntry()和remove()函数的话,那么仍然会存在内存泄漏问题。
参考:
1、Java并发编程:深入剖析ThreadLocal https://www.cnblogs.com/xiaoxi/p/7755253.html