理解ThreadLocal

概述

ThreadLocal是一种线程封闭技术,用于隔离线程间的数据,从而避免使用同步控制。

一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术称为线程封闭。

ThreadLocal为每条使用它的线程提供专属的内部变量。在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相互独立,互不影响。

基本用法

ThreadLocal对象通常被设计为类的私有静态类型(private static)字段,用来关联线程的某种状态。

举个例子:

public class ThreadLocalTest {

    private static ThreadLocal<Integer> counter = new ThreadLocal<Integer>() {
        protected Integer initialValue() {
            return new Integer(0);
        }
    };

    public static class MyRunnable implements Runnable {

        private void save() {
            System.out.printf("线程[%s]保存数据, 当前计数是: %s\n", Thread.currentThread().getId(), counter.get());
        }

        public void run() {
            while(true) {
                counter.set( (counter.get() + 1) % 10 );

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.printf("线程[%s]处理业务, 当前计数是: %s\n", Thread.currentThread().getId(), counter.get());

                if(counter.get() == 0) {
                    save();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable runnable = new MyRunnable();

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
    }

}

这个例子很简单,业务线程内有一个循环在不断的处理业务,假设每次处理业务会产生一些数据,出于性能考虑,希望每处理完10次业务才批量保存数据。

ThreadLocal主要有四个方法:

  • initialValue
  • get
  • set
  • remove(例子中未使用)

下面逐一简介。

initialValue方法

initialValue是设计给子类重写的方法,用以返回初始化的线程内部变量。在线程第一次调用get时它会被调用,但如果在调用get之前已经调用了set为线程内部变量设过值,则该方法不会被调用。所以,如果你希望手动调用set来初始化线程内部变量,则不必重写initialValue

通常initialValue只会被调用一次,除非手动调用remove清除了内部变量,之后又调用get方法,这时initialValue会再被调用初始化一个新的内部变量返回。

get方法

get用以获取ThreadLocal对象关联的线程内部变量。

public T get()

set方法

set用以设置ThreadLocal对象关联的线程内部变量的值。

public void set(T value)

remove方法

remove用以移除ThreadLocal对象关联的线程内部变量,某些情况需要用它来显式地移除,以防止内存泄漏。

public void remove()

你可能会问,为什么要这么复杂,在run里面使用一个方法局部变量来做计数器岂不是更简单。

对于这个例子来说,确实如此,ThreadLocal的功能性和方法局部变量没有本质的区别。

不过,ThreadLocal相较于方法局部变量,可以帮你管理线程内部变量,降低了同一线程内多个方法和组件间传递参数的复杂度。

内部实现

Paste_Image.png

如上图所示,ThreadLocal机制主要由EntryThreadLocalMapThreadThreadLocal这四个类相互协作实现的。

下面分析这四个类各自的职责和协作

Entry

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

Entry的定义很简单,它扩展自ThreadLocal类型的WeakReference类,是一个key-value对类。key是ThreadLocal对象的弱引用,value是线程的内部变量。

Entry使用弱引用作为key目的是,希望在外部不再需要访问ThreadLocal对象时可以让GC尽快地回收对象,而不必等到线程结束后。

当GC回收ThreadLocal对象后,再通过Entry.get()获取ThreadLocal对象时返回null,这使得内部能够感知什么时候不需要再持有对value的引用,从而释放Entry对象的引用,进而释放value的引用,这时如果value在外部没有任何引用的话(通常你不应该在外部持有对value的引用),随后被GC回收。这种感知和释放的行为发生在ThreadLocalgetsetremove操作时。

Thread

Thread内部持有一个ThreadLocalMap类型引用的成员变量。

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

threadLocals的初始值为null,它会延迟到初次访问时才实例化,即线程首个ThreadLocal对象调用get方法时才为threadLocals创建对象。

ThreadLocalMap

ThreadLocalMap是为ThreadLocal而设计的hash map,内部维护着一个哈希table数组,table内保存Entry的对象,通过ThreadLocal的哈希码可索引到(哈希码需转成数组下标)。

ThreadLocal

ThreadLocal是整个机制的总导演,对外,它提供使用的接口;对内,它协调类之间的相互协作。

ThreadLocal内部不会持有对线程内部变量的引用,线程内部变量的引用由Entry对象持有,而Entry对象寄存在ThreadLocalMap内的table中。

每一个ThreadLocal对象对应一个唯一的哈希码(threadLocalHashCode),通过这个哈希码可以从ThreadLocalMap中索引出对应的Entry,从而获得线程内部变量。

这里很巧妙,ThreadLocal对象与线程内部变量之间通过Entry对象间接关联,在内部只有Entry对象持有对ThreadLocal对象的弱引用,这样当外部不再使用ThreadLocal对象后,GC能够回收ThreadLocal对象,当内部探测到ThreadLocal对象被回收后就接着释放Entry对象。

最后

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。

通常在Java的世界里,我们不需要关系对象的释放,大部分情况下GC会自动帮我们回收。

但是如果使用ThreadLocal不当,是有可能导致内存泄漏的。

ThreadLocal释放内部变量通常在以下时机:

  • 线程结束后
  • 显式调用remove
  • 在调用getset时,如果探测到ThreadLocal对象的弱引用对象get返回null顺便释放。

所以,如果线程存活的生命周期很长,特别是和进程一样长的话,就要特别注意防止ThreadLocal引入内存泄漏的风险,在不需要再使用某个线程内部变量时记得显式调用remove清理掉。

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

推荐阅读更多精彩内容