[Java并发包学习七]解密ThreadLocal
概述
相信读者在网上也看了非常多关于ThreadLocal的资料,非常多博客都这样说:ThreadLocal为解决多线程程序的并发问题提供了一种新的思路;ThreadLocal的目的是为了解决多线程訪问资源时的共享问题。假设你也这样觉得的。那如今给你10秒钟,清空之前对ThreadLocal的错误的认知!
看看JDK中的源代码是怎么写的:
This class provides thread-local variables. These variables differ from
their normal counterparts in that each thread that accesses one (via its
{@code get} or {@code set} method) has its own, independently initialized
copy of the variable. {@code ThreadLocal} instances are typically private
static fields in classes that wish to associate state with a thread (e.g.,
a user ID or Transaction ID).
翻译过来大概是这种(英文不好,如有更好的翻译,请留言说明):
ThreadLocal类用来提供线程内部的局部变量。这样的变量在多线程环境下訪问(通过get或set方法訪问)时能保证各个线程里的变量相对独立于其它线程内的变量。
ThreadLocal实例通常来说都是
private类型的,用于关联线程和线程的上下文。
static
能够总结为一句话:ThreadLocal的作用是提供线程内的局部变量,这样的变量在线程的生命周期内起作用,降低同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
举个样例。我出门须要先坐公交再做地铁,这里的坐公交和坐地铁就好比是同一个线程内的两个函数。我就是一个线程,我要完毕这两个函数都须要同一个东西:公交卡(北京公交和地铁都使用公交卡),那么我为了不向这两个函数都传递公交卡这个变量(相当于不是一直带着公交卡上路),我能够这么做:将公交卡事先交给一个机构。当我须要刷卡的时候再向这个机构要公交卡(当然每次拿的都是同一张公交卡)。这样就能达到仅仅要是我(同一个线程)须要公交卡。何时何地都能向这个机构要的目的。
有人要说了:你能够将公交卡设置为全局变量啊,这样不是也能何时何地都能取公交卡吗?可是如果有非常多个人(非常多个线程)呢?大家可不能都使用同一张公交卡吧(我们如果公交卡是实名认证的),这样不就乱套了嘛。如今明确了吧?这就是ThreadLocal设计的初衷:提供线程内部的局部变量,在本线程内随时随地可取,隔离其它线程。
ThreadLocal基本操作
构造函数
ThreadLocal的构造函数签名是这种:
1 |
/** |
内部啥也没做。
initialValue函数
initialValue函数用来设置ThreadLocal的初始值。函数签名例如以下:
1 |
protected T initialValue() {
|
该函数在调用get函数的时候会第一次调用。可是假设一開始就调用了set函数,则该函数不会被调用。通常该函数仅仅会被调用一次,除非手动调用了remove函数之后又调用get函数。这样的情况下,get函数中还是会调用initialValue函数。
该函数是protected类型的,非常显然是建议在子类重载该函数的,所以通常该函数都会以匿名内部类的形式被重载,以指定初始值,比方:
1 |
package com.winwill.test; /** |
get函数
该函数用来获取与当前线程关联的ThreadLocal的值,函数签名例如以下:
1 |
public T get() |
假设当前线程没有该ThreadLocal的值,则调用initialValue函数获取初始值返回。
set函数
set函数用来设置当前线程的该ThreadLocal的值,函数签名例如以下:
1 |
public void set(T value) |
设置当前线程的ThreadLocal的值为value。
remove函数
remove函数用来将当前线程的ThreadLocal绑定的值删除。函数签名例如以下:
1 |
public void remove() |
在某些情况下须要手动调用该函数,防止内存泄露。
代码演示
学习了最主要的操作之后。我们用一段代码来演示ThreadLocal的使用方法,该样例实现以下这个场景:
有5个线程。这5个线程都有一个值value,初始值为0。线程执行时用一个循环往value值相加数字。
代码实现:
1 |
package com.winwill.test; /** |
运行结果为:
线程0的初始value:0
线程3的初始value:0
线程2的初始value:0
线程2的累加value:45
线程1的初始value:0
线程3的累加value:45
线程0的累加value:45
线程1的累加value:45
线程4的初始value:0
线程4的累加value:45
能够看到。各个线程的value值是相互独立的,本线程的累加操作不会影响到其它线程的值,真正达到了线程内部隔离的效果。
怎样实现的
看了基本介绍,也看了最简单的效果演示之后。我们更应该好好研究下ThreadLocal内部的实现原理。假设给你设计,你会怎么设计?相信大部分人会有这种想法:
每一个ThreadLocal类创建一个Map。然后用线程的ID作为Map的key。实例对象作为Map的value,这样就能达到各个线程的值隔离的效果。
没错。这是最简单的设计方案,JDK最早期的ThreadLocal就是这样设计的。JDK1.3(不确定是否是1.3)之后ThreadLocal的设计换了一种方式。
我们先看看JDK8的ThreadLocal的get方法的源代码:
1 |
public T get() {
|
当中getMap的源代码:
1 |
ThreadLocalMap getMap(Thread t) {
|
setInitialValue函数的源代码:
1 |
private T setInitialValue() {
|
createMap函数的源代码:
1 |
void createMap(Thread t, T firstValue) {
|
简单解析一下,get方法的流程是这种:
- 首先获取当前线程
- 依据当前线程获取一个Map
- 假设获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取相应的value e,否则转到5
- 假设e不为null。则返回e.value,否则转到5
- Map为空或者e为空,则通过
initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
然后须要注意的是Thread类中包括一个成员变量:
1 |
ThreadLocal.ThreadLocalMap threadLocals = null; |
所以,能够总结一下ThreadLocal的设计思路:
每一个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正须要存储的Object。
这个方法刚好与我们開始说的简单的设计方案相反。查阅了一下资料,这样设计的主要有下面几点优势:
- 这样设计之后每一个Map的Entry数量变小了:之前是Thread的数量。如今是ThreadLocal的数量,能提高性能,据说性能的提升不是一点两点(没有亲測)
- 当Thread销毁之后相应的ThreadLocalMap也就随之销毁了,能降低内存使用量。
再深入一点
先交代一个事实:ThreadLocalMap是使用ThreadLocal的弱引用作为Key的:
1 |
static class ThreadLocalMap {
/**
|
下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:

然后网上就传言,ThreadLocal会引发内存泄露。他们的理由是这种:
如上图。ThreadLocalMap使用ThreadLocal的弱引用作为key,假设一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry。就没有办法訪问这些key为null的Entry的value,假设当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄露。
我们来看看究竟会不会出现这样的情况。
事实上,在JDK的ThreadLocalMap的设计中已经考虑到这样的情况。也加上了一些防护措施。以下是ThreadLocalMap的getEntry方法的源代码:
1 |
private Entry getEntry(ThreadLocal<?
> key) {
|
getEntryAfterMiss函数的源代码:
1 |
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
|
expungeStaleEntry函数的源代码:
1 |
private int expungeStaleEntry(int staleSlot) {
|
整理一下ThreadLocalMap的getEntry函数的流程:
- 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e。假设e不为null而且key同样则返回e;
- 假设e为null或者key不一致则向下一个位置查询,假设下一个位置的key和当前须要查询的key相等,则返回相应的Entry。否则,假设key值为null,则擦除该位置的Entry,否则继续向下一个位置查询
在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链。自然会被回收。细致研究代码能够发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。
可是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。
这当然是不可能不论什么情况都成立的。所以非常多情况下须要使用者手动调用ThreadLocal的remove函数,手动删除不再须要的ThreadLocal。防止内存泄露。所以JDK建议将ThreadLocal变量定义成private的,这种话ThreadLocal的生命周期就更长,因为一直存在ThreadLocal的强引用。所以ThreadLocal也就不会被回收,也就能保证不论什么时候都能依据ThreadLocal的弱引用訪问到Entry的value值。然后remove它。防止内存泄露。
static
声明
[Java并发包学习七]解密ThreadLocal的更多相关文章
- Java并发编程(七)-- ThreadLocal
提到ThreadLocal,有些Android或者Java程序员可能有所陌生,可能会提出种种问题,它是做什么的,是不是和线程有关,怎么使用呢?等等问题,本文将总结一下我对ThreadLocal的理解和 ...
- Java编程思想学习(七) 抽象类和接口
1.抽象类和抽象方法 抽象方法:不完整的,仅有声明而没有方法体. abstract void f(); 抽象类:包含抽象方法的类.(若一个类包含一个或多个抽象方法,则该类必须限定为抽象的.) 1.用抽 ...
- Java并发包学习--ReentrantLock
这个锁叫可重入锁.它其实语义上和synchronized差不多,但是添加了一些拓展的特性. A reentrant mutual exclusion Lock with the same basic ...
- Java并发包学习一 ThreadFactory介绍
ThreadFactory翻译过来是线程工厂,顾名思义,就是用来创建线程的,它用到了工厂模式的思想.它通常和线程池一起使用,主要用来控制创建新线程时的一些行为,比如设置线程的优先级,名字等等.它是一个 ...
- [Java并发包学习八]深度剖析ConcurrentHashMap
转载自https://blog.csdn.net/WinWill2012/article/details/71626044 还记得大学快毕业的时候要准备找工作了,然后就看各种面试相关的书籍,还记得很多 ...
- JAVA并发包学习
1)CyclicBarrier一个同步辅助类,允许一组线程相互等待,直到这组线程都到达某个公共屏障点.该barrier在释放等待线程后可以重用,因此称为循环的barrier 2)CountDownLa ...
- java并发编程学习: ThreadLocal使用及原理
多线程应用中,如果希望一个变量隔离在某个线程内,即:该变量只能由某个线程本身可见,其它线程无法访问,那么ThreadLocal可以很方便的帮你做到这一点. 先来看一下示例: package yjmyz ...
- java并发包研究之-ConcurrentHashMap
概述 HashMap是非线程安全的,HashTable是线程安全的. 那个时候没怎么写Java代码,所以根本就没有听说过ConcurrentHashMap,只知道面试的时候就记住这句话就行了…至于为什 ...
- 20175314 《Java程序设计》第七周学习总结
20175314 <Java程序设计>第七周学习总结 教材学习内容总结 第八章:常用实用类 String()类代表字符串:Java 程序中的所有字符串字面值(如 "abc&quo ...
随机推荐
- node js 调试方法
1. node-debug tutorial 大家对nodejs调试应该都比较头疼,至少我这个不用IDE写js的人很头疼这个,其实node的生态圈非常好 有非常好的工具和非常潮的开发方式 这里总结了3 ...
- 优雅得使用composer来安装各种PHP小工具
Composer对php世界的影响是巨大的,使用composer来代替PEAR一定是大势所趋.当小伙伴们都还沉浸在composer带来的便利的时候,有没有想过如何更好的使用composer呢,网上大部 ...
- 基于python的接口测试框架设计(一)连接数据库
基于python的接口测试框架设计(一)连接数据库 首先是连接数据库的操作,最好是单独写在一个模块里, 然后便于方便的调用,基于把connection连接放在__init__()方法里 然后分别定义D ...
- LeetCode: Merge Intervals 解题报告
Merge IntervalsGiven a collection of intervals, merge all overlapping intervals. For example,Given [ ...
- Android开发:keytool' 不是内部或外部命令 也不是可运行的程序
今天在更改keystore密码的时候,发生了这个问题:keytool' 不是内部或外部命令 也不是可运行的程序. 本来以为很简单觉得的问题,在网上搜索了一大堆答案,都不是我想要的,故在此记录下我的解决 ...
- python的一些科学计算的包
在安装numpy这类科学计算的包的时候,pip下载的东西有时候缺少一些东西. 可以到这里下载,根据提示信息,少哪个包,或者哪个包出现错误就安装哪个包. PIL到这里下载
- python3打印26个英文字母
for a in range(ord('a'), ord('z') + 1): print(chr(a) , end="") for a in range(ord('A'), or ...
- multi-mechanize error: can not find test script: v_user.py问题
从github上下载,安装multi-mechanize,新建工程,运行工程报错. 环境: win7-x64, python 2.7 multi-mechanize can not find test ...
- CSS3 Transitions属性打造动画的下载按钮特效
一个网站的下载按钮应尽量吸引读者的注意. 这意味着网页设计师应该非常重视文件的下载界面.一个页面这么多的文件,如图片,视频和插件可以通过直接HTTP下载共享.许多免费网站甚至发布图标集和PSD文件供用 ...
- Rocket重试机制,消息模式,刷盘方式
一.Consumer 批量消费(推模式) 可以通过 consumer.setConsumeMessageBatchMaxSize(10);//每次拉取10条 这里需要分为2种情况 Consumer端先 ...