ThreadLocal以及增强

    xiaoxiao2023-09-21  157

    多线程的本质就是增加任务的并发,提高效率。但是又要控制任务不错乱,可以通过锁来控制资源的访问。

    除了控制资源的访问外,我们可以通过增加资源来保证所有对象的线程安全。比如100个人填写个人信息表,如果只有一支笔,那么大家都得排队,如果准备100支笔,这样人手一支笔,就可以很快完成填写信息。

    如果说锁是第一种思路,ThreadLocal就是第二种思路。

    ThreadLocal

    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也不能保证线程安全。

    ThreadLocal原理

    (上图来源:https://blog.csdn.net/aaronsimon/article/details/82711336)  

    每个Thread线程内部都有一个Map;Map里面存储线程本地对象(key)和线程的变量副本(value)Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。

    所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

     

    ThreadLocal源码

        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的弱引用问题则不存在了。

    InheritableThreadLocal

    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使用的正确姿势

    public static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new InheritableThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } };

    每次绑定时,都会产生一个新的对象。

    ThreadLocal局限性

    ThreadLocal它并不能解决线程安全问题,它旨在用于传递数据。但是它能成功传递数据比如有个大前提:放数据和取数据的操作必须是处于相同线程。

    InheritableThreadLocal,它能够支持跨线程传递数据,但也仅限于父线程给子线程来传递数据。但是对于没有任何关系的2个线程,它无能为力。

    线程池搭配问题

    由于线程池中是缓存使用过的线程,当线程被重复调用的时候并没有再重新初始化init()线程,而是直接使用已经创建过的线程,所以值并不会被再次操作。因为实际的项目中线程池的使用频率非常高,每一次从线程池中取出线程不能够直接使用之前缓存的变量,所以要解决这一个问题,网上大部分是推荐使用alibaba的开源项目transmittable-thread-local。

     

    TransmittableThreadLocal

    JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时的ThreadLocal值传递到 任务执行时。

    参考官网:https://github.com/alibaba/transmittable-thread-local

    源码解析参考:https://www.cnblogs.com/hama1993/p/10409740.html

    数据结构

     

    数据流程

    说明

    通过包装的ExecutorTtlWrapper提交Runnable时,一定不能是TtlRunnable,会抛出异常,只能是非TtlRunnalbe,会自动包装,保证每次运行时都会获取主线程的值。这样才有可能在主线程值变更后可以获取到。

     

     

     

     

     

     

    最新回复(0)