一、问题抛出
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(() -> { 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为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。这句话从字面上看起来很容易理解,但是真正理解并不是那么容易。
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.获取当前线程的map对象 ThreadLocalMap map = getMap(t); //3.如果map不为空,以threadlocal实例为key获取到对应Entry,然后从Entry中取出对象即可。 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); 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方法就可以获取实际的资源副本值。但是如果有一个为空,说明属于该线程的资源副本还不存在,则需要去创建资源副本,从代码中可以看到是调用setInitialValue()方法,其定义如下:
private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }
第8行调用initialValue()方法初始化一个值。接下来是判断线程的ThreadLocalMap是否为空,不为空就直接设置值(键为this,值为value),为空则创建一个Map,调用方法为createMap(),其定义如下:
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
在进行get之前,必须先set,否则会报空指针异常。 如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。因此如果没有执行set操作初始化Thread的threadLocals,则在创建ThreadLocal时必须重写initialValue()方法,否则会抛出异常:
private static ThreadLocal threadLocal = new ThreadLocal() { protected synchronized Object initialValue() { return new SimpleDateFormat(DATE_FORMAT); } };
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方法创建。
因此ThreadLocalThreadLocal为每个线程创建变量的副本的具体流程如下:
首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
三、ThreadLocal使用的一般步骤:
(1)在多线程的类(如ThreadDemo类)中,创建一个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就只属于这个线程了。
参考:
1、Java并发编程:深入剖析ThreadLocal https://www.cnblogs.com/xiaoxi/p/7755253.html