ThreadLocal趣谈 —— 杨过和他的四个冤家

一个一个上

一日醒来,杨过发现小龙女离家出走,于是外出寻找,不料碰上了金轮法王、李莫愁、裘千尺、公孙止四个冤家。

“哼,四个打我一个,算什么英雄好汉,有本事的,一个一个上!”

按照杨过的说法,这个场景,写成Java代码,大概就是这样:

public class ThreadSafeSDFUsingSync {
    private SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HHmm");

    public synchronized String formatIt(Date date) {
        return sdf.format(date);
    }
}

杨过就是这个线程不安全的SimpleDateFormat,一旦被多个线程同时操作(被多个高手同时进攻),就会出现异常(被打死),所以他选择了加锁,也就是synchronize,这样就不会有线程安全问题了。

为什么SimpleDateFormat是线程不安全的?这主要是因为,它内部使用了一个全局的Calendar变量,来存储date信息。详细解释可以参考文末列出的文章。

瞬间分身术

“呵呵,可笑,谁说我们是英雄好汉了?”,李莫愁说道。

说罢,四大高手一齐使出看家本领,欲置杨过于死地。

杨过先前在百花谷,学到了周伯通的左右互搏术,结合小时候看到的《火影忍者》里的影分身术,领悟出了自己的一套瞬间分身法。

只要有人向他进攻,他就能瞬间分身,去抵挡住对方的攻势。

写成代码,就是把上面的SimpleDateFormat,换成天然线程安全的局部变量,这样就无需使用synchronize加锁了:

public class ThreadSafeSDFUsingLocalVariable {
    public String formatIt(Date date) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HHmm");
        return sdf.format(date);
    }
}

分身大法

就这样双方僵持了两个小时,杨过发现这样打下去自己体力只会越来越差,因为每次四大高手中的任意一方发起进攻,自己都要花费内功产生一个分身(每次线程一调用,都需要去new一个对象)。

“能不能让分身不用完就消失呢?”,杨过一边应付攻势,一边思考着。

突然,他领悟出了一套可以持久分身的绝招,一下子分身出四个杨过,分别对付四个敌人。

写成代码,那就是用一个Map,key是线程ID,value是SimpleDateFormat,要用的时候,根据当前线程ID获取对应的SimpleDateFormat即可:

public class ThreadSafeSDFUsingMap {
    private Map<Long, SimpleDateFormat> sdfMap = new ConcurrentHashMap();

    public String formatIt(Date date) {
        Thread currentThread = Thread.currentThread();
        long threadId = currentThread.getId();

        SimpleDateFormat sdf = sdfMap.get(threadId);
        if (null == sdf) {
            sdf = new SimpleDateFormat("yyyyMMdd HHmm");
            sdfMap.put(threadId, sdf);
        }

        return sdf.format(date);
    }
}

当然,JDK早已经知道到我们会有这种需求,他们提供了ThreadLocal来帮助我们实现把变量和线程进行绑定的功能,上面的代码,可以用ThreadLocal进行改写:

public class ThreadSafeSDFUsingThreadLocal {
    private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal();

    static {
        formatter.set(new SimpleDateFormat("yyyyMMdd HHmm"));
    }

    public String formatIt(Date date) {
        SimpleDateFormat simpleDateFormat = formatter.get();
        return simpleDateFormat.format(date);
    }
}

使用ThreadLocal的静态方法withInitial,可以让上面这段代码更简洁。

简单看看ThreadLocal

ThreadLocal的实现思路,正如我们上面ThreadSafeSDFUsingMap所演示的,通过Map这样的key-value结构来将变量绑定到线程。

只不过这个Map不是常见的HashMap结构,这个Map也不是存储在ThreadLocal,并且Map的key也不是线程ID。

我们只需看一下ThreadLocal的set方法便可知道大概:

    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;
    }

set方法会先获取到当前线程,然后获取当前线程对象中,一个ThreadLocalMap类型的map,然后把自己,也就是threadLocal作为key,把要存储的值作为value,塞入这个map。

这张图很好的描述了Thread、ThreadLocal、ThreadLocalMap三者的关系:

为什么JDK要把数据放在Thread对象?而不直接放到ThreadLocal?为什么key值不是线程ID,而是ThreadLocal?思考题。后面再讨论。

ThreadLocal的另一个用途

上面讲的都是ThreadLocal在实现线程安全上的用途。

ThreadLocal还有另一个用途,那就是保存线程上下文信息。

这一点在很多框架乃至JDK类加载中都有用到。

比如Spring的事务管理,方法A里头调用了方法B,方法B如果失败了,需要执行connection.rollback()来回滚事务。

那么方法B怎么知道connection是哪个?最简单的就是方法A在调用方法B时,把connection对象传进去,伪代码如下:

@Transactional
methodA(){
  methodB(connection);
}

显然,这样很挫,需要修改方法的定义。

不过你现在知道ThreadLocal了,只需把connection塞入threadLocal,methodB和methodA在一个线程中执行,那么自然,methodB可以获取到和methodA相同的connection。

具体可以参考Spring的TransactionSynchronizationManager类,至于Spring的事务管理原理,后面再讨论。

总结

这篇文章带大家初步看了看ThreadLocal,了解了ThreadLocal的两大用途:

  • 实现线程安全;
  • 保存线程上下文信息

当然ThreadLocal肯定还有更多的用途,只要我们弄懂了它的原理,就知道如何灵活使用。

关于ThreadLocal的源码,比如:

  • 它和HashMap在key-value功能的实现上有何不同
  • 它为什么使用了WeakReference
  • 使用了WeakReference就不会有内存溢出的风险了吗?

咱们下回继续讨论。

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容