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

除了控制资源的访问外,我们可以通过增加资源来保证所有对象的线程安全。比如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 stubExecutorService 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个方法。操作的属性为ThreadinheritableThreadLocals属性。

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>() {@Overrideprotected 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,会自动包装,保证每次运行时都会获取主线程的值。这样才有可能在主线程值变更后可以获取到。

ThreadLocal以及增强相关推荐

  1. 这玩意比ThreadLocal叼多了,吓得我赶紧分享出来。

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! Dubbo的一次提交开始 故事得从前段时间翻阅 Dubbo ...

  2. 这玩意比 ThreadLocal 叼多了

    Dubbo的一次提交开始 故事得从前段时间翻阅 Dubbo 源码时,看到的一段代码讲起. 这段代码就是这个: org.apache.dubbo.rpc.RpcContext 使用 InternalTh ...

  3. threadlocal使用场景_这玩意比ThreadLocal叼多了,吓得我赶紧分享出来

    这是why哥的第 70 篇原创文章 Dubbo的一次提交开始 故事得从前段时间翻阅 Dubbo 源码时,看到的一段代码讲起. 这段代码就是这个: org.apache.dubbo.rpc.RpcCon ...

  4. 又踩到Dubbo的坑,但是这次我笑不出来

    前 言 直入主题,线上应用发现,偶发性出现如下异常日志 当然由于线上具体异常包含信息量过大,秉承让肥朝的粉丝没有难调试的代码的原则,我特意抽取了一个复现的demo放在了git,让你不在现场,一样享受到 ...

  5. JAVA跨线程传递数据方式总结

    实现跨线程传递数据方式: v1:子线程使用主线程的局部变量 这种当主线程和子线程不在一快儿时就不适用.可以使用JDK原生的InheritableThreadLocal. v2:InheritableT ...

  6. Spring AOP 增强框架 Nepxion Matrix 详解

    点击上方"方志朋",选择"置顶或者星标" 你的关注意义重大! 概述 在<深入聊一聊 Spring AOP 实现机制>一文中,介绍了 Spring A ...

  7. (转)Spring中ThreadLocal的认识

    我们知道Spring通过各种DAO模板类降低了开发者使用各种数据持久技术的难度.这些模板类都是线程安全的,也就是说,多个DAO可以复用同一个模板实例而不会发生冲突.我们使用模板类访问底层数据,根据持久 ...

  8. Spring Cloud中Hystrix 线程隔离导致ThreadLocal数据丢失(续)

    前言 上篇文章<Spring Cloud中Hystrix 线程隔离导致ThreadLocal数据丢失>我们对ThreadLocal数据丢失进行了详细的分析,并通过代码的方式复现了这个问题. ...

  9. spring线程并发处理(ThreadLocal)

    我们知道Spring通过各种DAO模板类降低了开发者使用各种数据持久技术的难度.这些模板类都是线程安全的,也就是说,多个DAO可以复用同一个模板实例而不会发生冲突. 我们使用模板类访问底层数据,根据持 ...

最新文章

  1. 模型大十倍,性能提升几倍?谷歌研究员进行了一番研究
  2. ubuntu16.04安装英伟达(NVIDIA)驱动——run文件安装
  3. java float 转double_将float转换为double而不会丢失精度
  4. ORACLE TEXT DATASTORE PREFERENCE(七)
  5. WordPress程序备受喜爱的原因:十八般武艺
  6. win11设置打开闪退怎么办,解决win11系统闪退的问题
  7. php 音频上传大小限制,WordPress最大上传文件大小限制修改 | Stay Curious
  8. OLAP-impala-大数据Week13-DAY6-impala
  9. 2串口两串口三串口多串口3串口转WiFi透传模块实现多通道与服务器透传
  10. php ctype xdigit,php ctype_digit() 函数介绍
  11. android 菜鸟面单打印_android菜鸟 实战项目之简单界面实现
  12. 如何免费使用内网穿透
  13. 数论--P8845 [传智杯 #4 初赛] 小卡和质数
  14. openGauss长沙Meetup | 共建数据库可信开源社区
  15. 微信分享内容给朋友、朋友圈、QQ、QQ空间等
  16. Android 相机开发
  17. Android 集成百度地图之申请TTS授权最新版
  18. 用于AD620系列仪表放大器的RFI抑制电路
  19. html5 index属性,深入理解CSS z-index属性
  20. spatialreg | 空间滞后模型、空间误差模型和空间杜宾模型简单形式的R语言实现...

热门文章

  1. hihoCoder1678 版本号排序
  2. 轻量级ORM框架Dapper应用四:使用Dapper返回多个结果集
  3. Golang 的跨平台交叉编译浅析
  4. Python字典get()方法的实际应用
  5. bootstrap大图轮播手机端不能手指滑动解决办法
  6. android studio 导入第三方库的记录
  7. Linux复习(六)
  8. Hanlder Looper MessageQueue Message
  9. 命令行刷新Magento索引管理
  10. Spark 实时电商数据分析及可视化