多线程的本质就是增加任务的并发,提高效率。但是又要控制任务不错乱,可以通过锁来控制资源的访问。
除了控制资源的访问外,我们可以通过增加资源来保证所有对象的线程安全。比如100个人填写个人信息表,如果只有一支笔,那么大家都得排队,如果准备100支笔,这样人手一支笔,就可以很快完成填写信息。
如果说锁是第一种思路,ThreadLocal就是第二种思路。
从ThreadLocal的名字上可以看到,这是一个线程的局部变量,也就是说只有当前线程可以访问,自然是线程安全的。
下面来看一个简单示例:
package main.java.study; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadLocalTest { private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static class ParseDate implements Runnable { int i = 0; public ParseDate(int i) { this.i = i; } public void run() { try { Date t = sdf.parse("2019-05-24 17:00:" + i % 60); System.out.println(i + ":" + t); } catch (ParseException e) { e.printStackTrace(); } } } public static void main(String[] args) { // TODO Auto-generated method stub ExecutorService es = Executors.newFixedThreadPool(10); for (int i = 0; i < 100; i++) { es.execute(new ParseDate(i)); } } }运行结果:
结果中即有正确的,又有错误的异常。出现这种问题的原因是SimpleDateFormat.parse()方法并不是线程安全的。因此在线程池中共享这个对象必然导致错误。
一种可行的方法是在sdf.parse()方法上加锁,这是一般思路,这里我们不这么做,我们使用ThreadLocal为每个线程都产生一个SimpleDateFormat对象。
package main.java.study;
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;
public class ThreadLocalTest2 {
static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();
public static class ParseDate implements Runnable { int i = 0;
public ParseDate(int i) { this.i = i; }
public void run() { try { if (tl.get() == null) { tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); //必须为ThreadLocal分配不同的对象,不然不能保证线程安全。 } Date t = tl.get().parse("2019-05-24 17:00:" + i % 60); System.out.println(i + ":" + t); } catch (ParseException e) { e.printStackTrace(); } } } public static void main(String[] args) { // TODO Auto-generated method stub ExecutorService es = Executors.newFixedThreadPool(10); for (int i = 0; i < 100; i++) { es.execute(new ParseDate(i)); } }
}
执行结果:
从上面可以看出,为每个线程人手分配一个对象工作并不是由ThreadLoca来完成,而是在应用层保证。如果在应用上为每个线程分配了同一个对象,则ThreadLocal也不能保证线程安全。
(上图来源:https://blog.csdn.net/aaronsimon/article/details/82711336)
每个Thread线程内部都有一个Map;Map里面存储线程本地对象(key)和线程的变量副本(value)Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return 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; }
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
ThreadLocal.ThreadLocalMap threadLocals = null;
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value;
Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
//每个ThreadLocal对象都有一个HashCode
private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
Thread.ThreadLocalMap<ThreadLocal, Object>;
1、Thread: 当前线程,可以通过Thread.currentThread()获取。
2、ThreadLocal:我们的static ThreadLocal变量。
3、Object: 当前线程共享变量。
我们调用ThreadLocal.get方法时,实际上是从当前线程中获取ThreadLocalMap<ThreadLocal, Object>,然后根据当前ThreadLocal获取当前线程共享变量Object。
ThreadLocal.set,ThreadLocal.remove实际上是同样的道理。
这种存储结构的好处:
1、线程死去的时候,线程共享变量ThreadLocalMap则销毁。
2、ThreadLocalMap<ThreadLocal,Object>键值对数量为ThreadLocal的数量,一般来说ThreadLocal数量很少,相比在ThreadLocal中用Map<Thread, Object>键值对存储线程共享变量(Thread数量一般来说比ThreadLocal数量多),性能提高很多。
关于ThreadLocalMap<ThreadLocal, Object>弱引用问题:
当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object>的键值对,造成内存泄露。(ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在)。
虽然ThreadLocal的get,set方法可以清除ThreadLocalMap中key为null的value,但是get,set方法在内存泄露后并不会必然调用,所以为了防止此类情况的出现,有两种手段。
1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;
2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。
ThreadLocal大部分情况下均能正常work。但是,在当下互联网环境下,经常会用到了异步方式来提高程序运行效率。当时当在主线程设置ThreadLocal变量,在子线程get TheadLocal变量时,未能获取到正确值。这是因为子线程与主线程不是同一个线程,因此获取不到主线程设置的变量值。
JDK扩展了ThreadLocal,实现了一个子类InheritableThreadLocal,它能够向子线程传递数据。
public class InheritableThreadLocal<T> extends ThreadLocal<T> { protected T childValue(T parentValue) { return parentValue; } ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } }
InheritableTheadLocal主要复写了getMap,createMap2个方法。操作的属性为Thread的inheritableThreadLocals属性。
ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;Thread类在构造时,会调用init方法
public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { if (name == null) { throw new NullPointerException("name cannot be null"); } this.name = name; //当前线程为子线程的父线程 Thread parent = currentThread(); 。。。。。。 //调用ThreadLocal的createInheritedMap 方法 if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Set thread ID */ tid = nextThreadID(); }createInheritedMap
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); } private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { //获取父线程的值, Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } } //这里是浅拷贝,与父值引用的是同一个引用,如果需要特殊处理,要覆盖此方法。 protected T childValue(T parentValue) { return parentValue; }上面解释为什么 父线程的InheritableThreadLocal变量可以传递给子线程。但是父线程或者子线程再次通过set命令赋值,不会互相影响。因为关系的建立仅在初始化子线程时建立。
ThreadLocal不能解决共享变量的线程安全问题,如果没有子线程,则安全问题可以保证,但是如果有子线程,多个子线程引用的是同一个对象,如果都对此对象的属性进行修改,则会导致线程安全问题。
每次绑定时,都会产生一个新的对象。
ThreadLocal它并不能解决线程安全问题,它旨在用于传递数据。但是它能成功传递数据比如有个大前提:放数据和取数据的操作必须是处于相同线程。
InheritableThreadLocal,它能够支持跨线程传递数据,但也仅限于父线程给子线程来传递数据。但是对于没有任何关系的2个线程,它无能为力。
由于线程池中是缓存使用过的线程,当线程被重复调用的时候并没有再重新初始化init()线程,而是直接使用已经创建过的线程,所以值并不会被再次操作。因为实际的项目中线程池的使用频率非常高,每一次从线程池中取出线程不能够直接使用之前缓存的变量,所以要解决这一个问题,网上大部分是推荐使用alibaba的开源项目transmittable-thread-local。
JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时的ThreadLocal值传递到 任务执行时。
参考官网:https://github.com/alibaba/transmittable-thread-local
源码解析参考:https://www.cnblogs.com/hama1993/p/10409740.html
通过包装的ExecutorTtlWrapper提交Runnable时,一定不能是TtlRunnable,会抛出异常,只能是非TtlRunnalbe,会自动包装,保证每次运行时都会获取主线程的值。这样才有可能在主线程值变更后可以获取到。