文档章节

解密ThreadLocal

zchuanzhao
 zchuanzhao
发布于 2017/04/06 15:45
字数 2860
阅读 16
收藏 1
点赞 0
评论 0

相信读者在网上也看了很多关于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的构造函数签名是这样的:

/**

* Creates a thread local variable.

* @see #withInitial(java.util.function.Supplier)

*/

public ThreadLocal() {

}

内部啥也没做。

initialValue函数

initialValue函数用来设置ThreadLocal的初始值,函数签名如下:

protected T initialValue() {

return null;

}


该函数在调用get函数的时候会第一次调用,但是如果一开始就调用了set函数,则该函数不会被调用。通常该函数只会被调用一次,除非手动调用了remove函数之后又调用get函数,这种情况下,get函数中还是会调用initialValue函数。该函数是protected类型的,很显然是建议在子类重载该函数的,所以通常该函数都会以匿名内部类的形式被重载,以指定初始值,比如

package com.winwill.test;


public class TestThreadLocal {

private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {

@Override

protected Integer initialValue() {

return Integer.valueOf(1);

}

};

}


该函数用来获取与当前线程关联的ThreadLocal的值,函数签名如下:get函数

public T get()


set函数如果当前线程没有该ThreadLocal的值,则调用initialValue函数获取初始值返回。

set函数用来设置当前线程的该ThreadLocal的值,函数签名如下:

public void set(T value)


remove函数设置当前线程的ThreadLocal的值为value。

remove函数用来将当前线程的ThreadLocal绑定的值删除,函数签名如下:

public void remove()


代码演示在某些情况下需要手动调用该函数,防止内存泄露。

学习了最基本的操作之后,我们用一段代码来演示ThreadLocal的用法,该例子实现下面这个场景:

有5个线程,这5个线程都有一个值value,初始值为0,线程运行时用一个循环往value值相加数字。

代码实现:

package com.winwill.test;


public class TestThreadLocal {

private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {

@Override

protected Integer initialValue() {

return 0;

}

};

public static void main(String[] args) {

for (int i = 0; i < 5; i++) {

new Thread(new MyThread(i)).start();

}

}

static class MyThread implements Runnable {

private int index;

public MyThread(int index) {

this.index = index;

}

public void run() {

System.out.println("线程" + index + "的初始value:" + value.get());

for (int i = 0; i < 10; i++) {

value.set(value.get() + i);

}

System.out.println("线程" + index + "的累加value:" + value.get());

}

}

}


线程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方法的源码:

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();

}

其中getMap的源码:

ThreadLocalMap getMap(Thread t) {

return t.threadLocals;

}

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;

}

 

createMap函数的源码:

void createMap(Thread t, T firstValue) {

t.threadLocals = new ThreadLocalMap(this, firstValue);

}

简单解析一下,get方法的流程是这样的:

  1. 首先获取当前线程
  2. 根据当前线程获取一个Map
  3. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的value e,否则转到5
  4. 如果e不为null,则返回e.value,否则转到5
  5. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

然后需要注意的是Thread类中包含一个成员变量:

ThreadLocal.ThreadLocalMap threadLocals = null;

所以,可以总结一下ThreadLocal的设计思路:
每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。
这个方案刚好与我们开始说的简单的设计方案相反。查阅了一下资料,这样设计的主要有以下几点优势:

  • 这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能,据说性能的提升不是一点两点(没有亲测)
  • 当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。

再深入一点

先交代一个事实:ThreadLocalMap是使用ThreadLocal的弱引用作为Key的

static class ThreadLocalMap {

/**

* The entries in this hash map extend WeakReference, using

* its main ref field as the key (which is always a

* ThreadLocal object). Note that null keys (i.e. entry.get()

* == null) mean that the key is no longer referenced, so the

* entry can be expunged from table. Such entries are referred to

* as "stale entries" in the code that follows.

*/

static class Entry extends WeakReference<ThreadLocal<?>> {

/** The value associated with this ThreadLocal. */

Object value;

Entry(ThreadLocal<?> k, Object v) {

super(k);

value = v;

}

}

...

...

}

下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:

然后网上就传言,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方法的源码:

private Entry getEntry(ThreadLocal<?> key) {

int i = key.threadLocalHashCode & (table.length - 1);

Entry e = table[i];

if (e != null && e.get() == key)

return e;

else

return getEntryAfterMiss(key, i, e);

}

 

getEntryAfterMiss函数的源码:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {

Entry[] tab = table;

int len = tab.length;

while (e != null) {

ThreadLocal<?> k = e.get();

if (k == key)

return e;

if (k == null)

expungeStaleEntry(i);

else

i = nextIndex(i, len);

e = tab[i];

}

return null;

}

expungeStaleEntry函数的源码:

private int expungeStaleEntry(int staleSlot) {

Entry[] tab = table;

int len = tab.length;

// expunge entry at staleSlot

tab[staleSlot].value = null;

tab[staleSlot] = null;

size--;

// Rehash until we encounter null

Entry e;

int i;

for (i = nextIndex(staleSlot, len);

(e = tab[i]) != null;

i = nextIndex(i, len)) {

ThreadLocal<?> k = e.get();

if (k == null) {

e.value = null;

tab[i] = null;

size--;

} else {

int h = k.threadLocalHashCode & (len - 1);

if (h != i) {

tab[i] = null;

// Unlike Knuth 6.4 Algorithm R, we must scan until

// null because multiple entries could have been stale.

while (tab[h] != null)

h = nextIndex(h, len);

tab[h] = e;

}

}

}

return i;

}

整理一下ThreadLocalMap的getEntry函数的流程:

  1. 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
  2. 如果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 static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

本文转载自:http://qifuguang.me/2015/09/02/[Java%E5%B9%B6%E5%8F%91%E5%8C%85%E5%AD%A6%E4%B9%A0%E4%B8%83]%E8%A7%A3

共有 人打赏支持
zchuanzhao
粉丝 45
博文 233
码字总数 142836
作品 1
福州
程序员
基于ClassLoader的java代码加密的经验分享

原理就是 生成项目时将待加密的java class文件通过加密算法转换生成加密的二进制文件,此文件不会被JD-GUI等反编译工具直接解密。 项目在启动时,用自定义的ClassLoader将加密的二进制文件进...

hxt168
06/06
0
0
android 3des加密Could not instantiate bean class ...

com.sun.crypto.provider.SunJCE Could not instantiate bean class [com.lz.monitor.alert.service.ServiceImp]: Constructor threw exception; nested exception is java.lang.NoClassDefF......

大凉龙雀
2012/07/24
0
0
关于AES256算法java端加密,ios端解密出现无法解密问题的解决方案

我想关于AES算法大家应该都已经了解了,我就不多介绍了。这是本人第一次写技术博文,如果有不对之处欢迎大家指正,共同讨论,一起学习! 之前在项目上用到AES256加密解密算法,刚开始在java端...

Sun1009
2012/12/13
0
32
关于一个RSA跨语言(java 和py)的加密解密操作

现在有一个需求,java平台提供接口,python平台调用接口,中间用到rsa进行接口的加密解密。java服务端的rsa加密操作已经完成,返回一个16进制的字符串给python平台,但是在python进行私钥解密...

我亦暖心丶
07/17
0
0
微信小程序开发实战1:通过shareTicket获取分享群的OpenGId

玩过微信小程序的都知道,如果有群排行就必然要获取群的唯一id(OpenGId)。 但是OpenGId的获取必须通过加密数据:encryptedData、加密向量iv获取,对于encryptedData的解密微信官方提供了P...

qicong88
05/18
0
0
Java加密技术(七)——非对称加密算法最高级ECC

Java加密技术(七)——非对称加密算法最高级ECC 博客分类: Java/Security Java非对称加密算法ECC ECC ECC-Elliptic Curves Cryptography,椭圆曲线密码编码学,是目前已知的公钥体制中,对...

脸大的都是胖纸
2015/03/19
0
0
在java中用AES加密,然后再node.js中解密

现在碰到这个问题: 在java中用AES加密,然后再node.js中解密 ,node.js 解密不出来 调研尝试很久了,碰壁啊 有牛人吗? java端: AES/ECB/PKCS5Padding public static String encryptAES(S...

qingfeng哥
2013/12/23
0
2
Objective-C 和 Java 下 DES加解密保持一致的方式

最近做了一个移动项目,是有服务器和客户端类型的项目,客户端是要登录才行的,登录的密码要用DES加密,服务器是用Java开发的,客户端要同时支持多平台(Android、iOS),在处理iOS的DES加密...

山哥
2012/04/19
0
5
使用Hutool处理RSA等非对称加密

介绍 Hutool工具是一个国产开源Java工具集,旨在简化Java开发中繁琐的过程,Hutool-crypto模块便是针对JDK加密解密做了大大简化。 此文主要介绍利用Hutool-crypto简化非对称加密解密。 对于非...

路小磊
2017/08/24
0
22
Android外部文件加解密及应用实践

有这样的应用场景,当我们把一些重要文件放到asset文件夹中时,把.apk解压是可以直接拿到这个文件的,一些涉及到重要信息的文件我们并不想被反编译拿去,这个时候需要先对文件进行加密,然后...

C6C
05/08
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

对基于深度神经网络的Auto Encoder用于异常检测的一些思考

一、前言 现实中,大部分数据都是无标签的,人和动物多数情况下都是通过无监督学习获取概念,故而无监督学习拥有广阔的业务场景。举几个场景:网络流量是正常流量还是攻击流量、视频中的人的...

冷血狂魔
12分钟前
0
0
并发设计之A系统调用B系统

A-->B A在发送请求之前,用乐观锁,减少对B的重复调用,这样一定程度上是幂等性。 比如A系统支付功能,要调用B系统进行支付操作,但是前端对"支付"按钮不进行控制,即用户会不断多次点击支付...

汉斯-冯-拉特
32分钟前
0
0
HTTP协议通信原理

了解HTTP HTTP(HyperText Transfer Protocol)是一套计算机通过网络进行通信的规则。计算机专家设计出HTTP,使HTTP客户(如Web浏览器)能够从HTTP服务器(Web服务器)请求信息和服务。 HTTP使用...

寰宇01
55分钟前
0
0
【Java动态性】之反射机制

一、Java反射机制简介

谢余峰
56分钟前
1
0
Centos 6.X 部署环境搭建

1.Linux学习笔记CentOS 6.5(一)--CentOS 6.5安装过程

IT追寻者
今天
0
0
博客即同步至腾讯云+社区声明

我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=8vy9bsmadbko...

xiaoge2016
今天
1
0
大数据教程(3.1):Linux系统搭建网络YUM源服务器

博主在前面的2.5章节讲述了linux系统本地YUM服务器的搭建和httpd轻量级静态网站服务器的安装,本节博主将为大家分享内网环境中搭建自己的网络YUM服务器的全过程。如果大家对本地YUM服务器还不...

em_aaron
今天
1
0
蚂蚁技术专家:一篇文章带你学习分布式事务

小蚂蚁说: 分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在这几年越来越火的微服务架构中,几乎可以说是无法避免,本文就围绕分布式事务...

Java大蜗牛
今天
1
0
新的Steam应用将拓展服务项目

导读 未来几周,Steam将推出两个免费的应用程序Steam Link和Steam Video。这两个应用程序都旨在拓展Steam平台的业务和便利性。 即将开放的Steam Link应用程序最先提供了Android测试版,它将允...

问题终结者
今天
0
0
golang 第三方包的使用总结

golang 第三方包的安装的方法: 1. go get 安装 $ go get github.com/gin-gonic/gin 注意:执行go get 命令需要先安装git命令,并配置git全局变量。 2. 源码包安装 由于国内网络问题,很多时...

科陆李明
今天
1
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部