你和ThreadLocal的牵手成功,仅仅差这篇"恋爱攻略"了

不管是逛帖子还是刷面试题,我们经常会看到ThreadLocal的身影,

不禁想问这玩意到底是干嘛的,今天我们以四个章节

「初识」,「相知」,「相恋」,「携手」

来由浅入深的了解下ThreadLocal,看完后我想就应该能牵手成功,彻底拿下ThreadLocal了。

(猴急的人可以只看「相知」章节)


初识

她是谁?第一眼看上去,名字里有个Thread,貌似和Thread有点关系?事实确实如此,

但注意她可不是线程。要想弄清她的身世,最正确的方式不是百度,而是去她的出生地,源码里一探究竟。

第一句话就明确写明了:This class provides thread-local variables 这个类提供线程局部变量

紧接着 that each thread that accesses one  has its own,independently initialized copy of the variable. 

为每个线程提供独立的副本(为线程自身独有,不共享)。

到这里就有了初步的认识:她是每个线程都有的(或者说提供给每个线程),并且是独有的不共享的局部变量(就像老婆一样,不共享,,,)

继续往下看,

For example, the class below generates unique identifiers local to each thread。A thread's id is assigned the first time it invokes and remains unchanged on subsequent calls。

比如,下面的类为每个线程生成唯一的标识符,线程的id在第一次调用时被分配,在随后的调用中保持不变,啥意思?不急先来看看后面的小demo(JDK里面有很多关键类,类注释里都会写个简单的demo,帮我们快速了解类的用途比如Thread):

public class ThreadId {

     // Atomic integer containing the next thread ID to be assigned

     private static final AtomicInteger nextId = new AtomicInteger(0);

     // Thread local variable containing each thread's ID

     private static final ThreadLocal<Integer> threadId =

         new ThreadLocal<Integer>() {

             @Override

              protected Integer initialValue() {

                 return nextId.getAndIncrement();

         }

     };

     // Returns the current thread's unique ID, assigning it if necessary

     public static int get() {

         return threadId.get();

     }

}

这个demo主要做了三件事

1.声明了个AtomicInteger 类型的变量nextId,其实这里你也可以直接理解为int类型

2.创建了ThreadLocal对象threadId,并且重写了initialValue方法,这里面会将初始值(0)自增

3.提供get方法,返回threadId里的值

还是不太懂?没关系,这里其实还没有正式的使用,只是定义了下。可以看出ThreadLocal实际上就是一个普通的对象而已。我们通过重写它的匿名内部类的方法来实现变量的存或者取的功能。



相知

已经了解了她的身世,现在我们就要更近一步,揭开她的面纱。

折叠下源码,可以看到除了demo里的initialValue和get方法外,还有set方法,日常使用比较多的就是这3个,下面我们就来正式的使用它。

ThreadLocal的部分方法

试想有一个这样的场景:工地上,工人往小车里搬砖,小车的最大装砖数量固定,当装满后车就会被运走清空再运回来,工人们继续往里面搬砖,工地的经理每隔段时间就会监工,看看工人们搬了多少砖,有没有偷懒。

抽象一下,实际上就3个实体:工人、经理和小车,工人和经理的工作都是耗时过程,放在线程中进行,小车相当于一个容器,用来存放砖头。当然还需要一个场所,用来开展这些活动。因此我们得出以下代码:

/**

* 工地类,在这里有三个角色

* 1.工人:工人负责往小车里搬砖,每个人的搬砖速度不一样

* 2.经理:经理负责监工,每隔断时间就会去检查工人们的搬砖数量

* 3.小车:小车用来装砖头,达到最大数量时,小车就会被运走清空再运回来,

*  每运一次称为一趟(中间时间花费忽略不计)

* @author 杰洛特

*/

public class WorkSite {

// 小车能装的最大砖头数量

public static final int MAX_COUNT = 4;

// 装砖的小车

public static final ThreadLocal<CarInfo> workCar = new ThreadLocal<CarInfo>() {

@Override

public void set(CarInfo carInfo) {

// 每次装到最大值时,车就被运走清空,多出的砖就装到新的一趟中(趟数+1)

if (carInfo.getCount() > MAX_COUNT) {

carInfo.setTimes(carInfo.getTimes() + 1);

carInfo.setCount(carInfo.getCount() - MAX_COUNT);

}

super.set(carInfo);

}

@Override

protected CarInfo initialValue() {

System.out.println(Thread.currentThread().getName() + "的小车已经到位");

// 第一次(0趟)使用小车,里面还是空的(0块砖)

CarInfo info = new CarInfo();

info.setOwner(Thread.currentThread().getName());

info.setCount(0);

info.setTimes(0);

return info;

}

};

public static void main(String[] args) {

Worker one = new Worker("工人1", workCar, 1);

Worker two = new Worker("工人2", workCar, 2);

one.startWork();

two.startWork();

Manager manager = new Manager(one, two);

manager.startCheck();

}

}

在工地类中,有个ThreadLocal类型的小车成员变量workCar,且泛型为CarInfo,里面记录了小车的趟数以及砖头数量和拥有者的姓名:

/**

* 小车的信息类

* @author 杰洛特

*/

public class CarInfo {

//趟数

private int times;

//砖头数量

private int count;

//拥有者(工人的名字)

private String owner;

public int getTimes() {

return times;

}

public void setTimes(int times) {

this.times = times;

}

public int getCount() {

return count;

}

public void setCount(int count) {

this.count = count;

}

public String getOwner() {

return owner;

}

public void setOwner(String owner) {

this.owner = owner;

}

}

在ThreadLocal的initialValue方法里,初始化了小车的一些属性,在set方法里加了些逻辑:如果小车被装满了那么就装新的一趟,这里没有重写get方法,是因为get方法不需要添加逻辑。

接着,在main方法中,创建了两个Worker类型的工人对象one和two,并且给他们分给了一人一辆小车(虽然是同一个workCar对象,但是每个工人都独有里面的CarInfo 信息,并不共享,这就是ThreadLocal的特别之处)。两个工人的力气不一样大,工人1一次只能搬1块砖,工人2一次可以搬2块砖。

/**

* 工人类,负责往小车里搬砖,不需要关注小车的情况

* @author 杰洛特

*/

public class Worker {

// 工人名字

private String name;

// 工作时间(这里偷懒直接用了布尔值)

boolean workTime = true;

// 每次搬砖数

private int num = 0;

// 总搬砖数

private int totalCount = 0;

// 小车

private ThreadLocal<CarInfo> car;

public Worker(String name, final ThreadLocal<CarInfo> car, int num) {

this.name = name;

this.car = car;

this.num = num;

}

public String getName() {

return name;

}

public void setName(String workerName) {

this.name = workerName;

}

public boolean isWorkTime() {

return workTime;

}

public void setTotalCount(int trip) {

this.totalCount = trip;

}

public int getTotalCount() {

return totalCount;

};

// 开始干活

public void startWork() {

Thread workerThread = new Thread(name) {

@Override

public void run() {

super.run();

while (isWorkTime()) {

CarInfo currentCar = car.get();

// 每次能往车里搬num块砖

currentCar.setCount(currentCar.getCount() + num);

car.set(currentCar);

System.out.println(currentCar.getOwner() + "搬第" + (currentCar.getTimes() + 1) + "趟的第"

+ currentCar.getCount() + "块砖");

// 记录下自己搬了多少块砖

// 总搬砖数 = 小车的趟数 * 每趟的砖数 + 现在已经搬到车中的砖头数

setTotalCount(currentCar.getTimes() * WorkSite.MAX_COUNT + currentCar.getCount());

try {

// 休息1S

TimeUnit.SECONDS.sleep(1);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

// 下班了,不再需要小车,归还(如果不归还,经理会以为工人还在工作,但实际是在休息造成误解)

car.remove();

}

};

workerThread.start();

}

}

工人类里开了子线程,在子线程里获取到工人对应的小车,并往里面搬砖,工人不需要关心小车是否装满、是第几趟(因为这些逻辑在小车的set方法里已经实现了),只需要专注搬砖即可。当下班了后,还需要调用remove方法,移除小车,否则可能会造成内存泄漏。

在工地类的最后,还创建了个Manager类型的经理类,它会检查工人们的总搬砖数量。

/**

* 经理类,定时检查每个工人的搬砖数量

* @author 杰洛特

*/

public class Manager {

//检查时间,偷懒直接设置成了布尔型

private boolean isCheckTime = true;

private Worker[] works;

public Manager(Worker... workers) {

this.works = workers;

}

public void startCheck() {

Thread managerThread = new Thread("经理") {

@Override

public void run() {

super.run();

while (isCheckTime) {

for (int i = 0; i < works.length; i++) {

System.out.println(Thread.currentThread().getName() + "开始检查" + works[i].getName() + ",他共搬了" + works[i].getTotalCount() + "块砖");

}

try {

// 每隔5秒检查下

TimeUnit.SECONDS.sleep(5);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

};

managerThread.start();

}

}

运行程序,输出以下结果(因为没有结束条件,所以会一直运行,需要手动结束):

运行结果

可以看到,因为工人2比工人1有力气,每次搬的数量是他的两倍,当经理每次检查的时候,两个工人搬的总砖数也符合这个比例。对这个例子做个总结:创建了一个ThreadLocal的对象,这个对象分别为两个线程(工人1和工人2)提供了一个本地的变量(carInfo),让每个工人都拥有了自己的小车,小车里的砖头数量互不影响。小车里记录了已完成的趟数和已放入的砖头数,工人只需要将已完成的趟数乘以每趟的最大砖头数量再加上当前已放入的砖头数就可以得出总的搬砖数了,最后将结果告诉前来检查的经理。

看到这里应该清楚ThreadLocal的作用以及使用方式了吧(可以再回到[初识]章节回味下)?

简单的说,ThreadLocal是一个泛型对象,它为线程提供了一个泛型的局部变量,并且在ThreadLocal中可以对这个局部变量的初始化、存和取等操作添加逻辑,所有使用到这个对象的线程都共用这些逻辑。也就是说ThreadLocal不仅能为每个线程提供独立的局部变量还能为这些线程提供统一的变量处理逻辑。


相恋

经过以上章节,我们终于知道ThreadLocal是什么以及怎么使用了,不过这也只是个开始,下面我们要深入了解她。

为什么一个ThreadLocal对象能给多个线程提供互不干扰的本地变量呢?这个就要先从set、get方法入手了。

直接上源码:

set方法

可以看到set方法先得到所在的线程,然后根据线程通过getMap方法获取到对应的ThreadLocalMap,getMap()如下:

getMap方法

getMap方法会返回Thread的threadLocals成员变量。我们再去Thread类中看看threadLocals长什么样。

Thread中定义的threadLocals

实际上ThreadLocalMap是ThreadLocal的静态内部类:

ThreadLocalMap

可以看到ThreadLocalMap是一种类似hash map的数据结构,key为ThreadLocal,value为Object,对应搬砖的例子,key就是workCar,value就是carInfo。回到set方法中,第一次执行getMap(t)返回值肯定为null,所以进入createMap分支:

createMap方法

createMap 里很简单,就是new了个ThreadLocalMap并把它赋给了Thread的threadLocals成员变量。让我们再看看get方法:

get方法

和set方法类似,会先获取到当前线程对应的map,如果不为空再传入自己(key为this),返回对应的value,如果为空那么执行setInitialValue方法,这方法会去调用initialValue方法,实现也很简单,源码我就不贴了。

简单的说ThreadLocal的原理就是:每个线程都有一个ThreadLocalMap成员变量,当在线程中调用ThreadLocal对象的set/get方法时,会去对这个map进行存或者取的操作。这也是ThreadLocal能实现本地变量线程隔离的原因所在。换句话说,Thread里本来就有个本地变量,只不过你不能直接赋值或使用,而要通过ThreadLocal对象去间接操作。有人说这样做的意义是什么?别忘了ThreadLocal里你可以重写set/get方法,相当于可以提供统一的处理逻辑,就像是搬砖例子中的小车一样,不管是哪个工人(线程)往里面搬(set方法),它一趟最多装砖数都是固定的,并且装满后会清空运走再运回来(第二趟),工人们不需要自己判断小车的状态,只需要无脑搬砖就可以了。



携手

看到这里,恭喜你,已经获得了ThreadLocal的芳心!但若想让这份感情走的持久、甜蜜,我们还需要注意以下几点:

1.创建ThreadLocal对象时,其修饰符应该是 static final ,[初识]章节官方的那个小demo也是如此。

2.当不需要ThreadLocal的时候,应该及时调用remove方法,remove会切断Thread和ThreadLocal之间的联系(移除ThreadLocalMap中的item),就像是工人下班,需要把小车归还(remove)一样,如果不归还那么即使到了下班时间,还是不能休息。以上两点其目的就是为了防止多次使用ThreadLocal后造成的内存泄漏。

3.ThreadLocal也可以被其它实现方式替代,但是不如直接使用ThreadLocal简单快捷,并且相比synchronized等同步的方式具有更高的并发性。

                                                                                                                                                              【原创文章,转载请标明出处】

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

推荐阅读更多精彩内容

  • CHANGE LOG v0.1 2018/07/17 Chuck Chan 示例 我们先来看 ThreadLoca...
    ChuckChan阅读 1,164评论 0 49
  • ThreadLocal,直译为“线程本地”或“本地线程”,如果你真的这么认为,那就错了!其实,它就是一个容器,用于...
    朦胧蜜桃阅读 254评论 1 0
  • 一、ThreadLocal 适合用在哪些实际生产的场景中 保存每个线程独享的对象为每个线程都创建一个副本,这样每个...
    Travis_Wu阅读 248评论 0 1
  • 原理 产生线程安全问题的根源在于多线程之间的数据共享。如果没有数据共享,就没有多线程并发安全问题。ThreadLo...
    Java耕耘者阅读 304评论 0 0
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,523评论 16 22