ThreadLocal中优雅的数据结构如何体现农夫山泉的广告语
本篇文章主要讲解 ThreadLocal 的用法和内部的数据结构及实现。有时候我们写代码的时候,不太注重类之间的职责划分,经常造出一些上帝类,也就是什么功能都往这个类里放。虽然能实现功能但是并不优雅且不好维护。这篇文章就介绍 ThreadLocal 中如何设计优雅的数据结构以及类之间的职责划分,至于怎么跟农夫山泉广告语扯上关系,相信你读完便有了答案,文末也有解释。
用法
ThreadLocal 对象可以当做每个线程局部变量。也就是不同线程同时读写同一个 ThreadLocal 对象,其实操作的是线程本地的数据, 所以不存在线程安全问题。听起来跟我们常规的理解有点矛盾,下面就举个例子看看
package com.cnblogs.duma.thread;
public class ThreadLocalTest {
// 定义 ThreadLocal 对象
static ThreadLocal<String> var1 = new ThreadLocal<String>(); public static void main(String args[]) {
new Thread1().start();
new Thread1().start();
} public static class Thread1 extends Thread {
@Override
public void run() {
// 写同一个 ThreadLocal 对象 —— var1
var1.set(getName());
while (true) {
System.out.println(getName() + " get var1: " + var1.get());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
控制台输出
Thread-0 get var1: Thread-0
Thread-1 get var1: Thread-1
Thread-1 get var1: Thread-1
Thread-0 get var1: Thread-0
Thread-1 get var1: Thread-1
Thread-0 get var1: Thread-0
本例中,我们定义了 ThreadLocal 对象 static ThreadLocal<String> var1 = new ThreadLocal<String>(); ,一般定义 ThreadLocal 对象都用 static 修饰,因为它的作用是提供读写线程本地属性的能力,与对象没关系,所以定义成 static 即可。从控制台输出可以看出,所有线程都读写 var1 变量, 但互不干扰。看到这里,你可能会猜想,是不是 ThreadLocal 的 set 方法将线程做 key(每个线程都是独一无二的),将我们要设置的值作为 value,存到一个 公共的 map 结构中,如果这样想只对了一半, 下面我会对照源码详细介绍。
看到这里你可以又有疑问,为什么不在线程内部定义个局部变量,不是也能实现 ThreadLocal 的功能。为了解答这个问题,我们来看看下面的例子
package com.cnblogs.duma.thread; public class ThreadLocalTest {
// 定义 ThreadLocal 对象
static ThreadLocal<String> var1 = new ThreadLocal<String>(); public static void main(String args[]) {
new Thread1().start();
new Thread1().start();
new Thread2().start();
} public static class Thread1 extends Thread {
@Override
public void run() {
// 写同一个 ThreadLocal 对象 —— var1
var1.set(getName());
while (true) {
System.out.println(getName() + " get var1: " + var1.get());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} public static class Thread2 extends Thread {
@Override
public void run() {
// 写同一个 ThreadLocal 对象 —— var1
var1.set(getName());
while (true) {
System.out.println(getName() + " get var1: " + var1.get());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
控制台输出
Thread-0 get var1: Thread-0
Thread-2 get var1: Thread-2
Thread-1 get var1: Thread-1
Thread-0 get var1: Thread-0
Thread-2 get var1: Thread-2
Thread-1 get var1: Thread-1
这个例子是在第一个基础上又定义了一个线程类 —— Thread2,它同样可以操作 var1 并且也是操作本地的变量,与 Thread1 线程互不干扰。所以,我们就知道 ThreadLocal 与局部变量的区别了, 它可以为所有线程提供本地数据的读写能力。
原理
既然用法了解了,那我们就深入到 ThreadLocal 类的内部一探究竟,看看它的实现跟我们刚才的猜测有什么区别。在 ThreadLocal 中最重要的两个方法是 set 和 get,下面我们分别看下这两个方法的源码
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);
} // 获取当前线程的 threadLocals 属性
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
} void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
在 set 方法中, 首先调用了 getMap 方法,getMap 方法返回的是当前线程的 threadLocals 属性,如果当前线程的 threadLocals 属性为空,需要初始化,即调用 createMap 方法,在该方法中,new 一个 ThreadLocalMap 对象,该类是 ThreadLocal 的内部类。因此,我们可以看到并不是有一个全局的 map 保存数据,而确实是每个线程本地的局部变量(threadLocals)。既然每个线程操作是属于自己的变量,那么把 ThreadLocalMap 对象放在线程本地自然就更合理。接下来看看 ThreadLocalMap 的构造方法
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode =
new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
} static class ThreadLocalMap {
private static final int INITIAL_CAPACITY = 16; private java.lang.ThreadLocal.ThreadLocalMap.Entry[] table; private int size = 0; static class Entry extends WeakReference<java.lang.ThreadLocal<?>> {
Object value; Entry(java.lang.ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
} ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// table 用来存储不同 ThreadLocal 对象对应的值
table = new Entry[INITIAL_CAPACITY];
// 每一个 ThreadLocal 对象都有一个 threadLocalHashCode 属性,即 hash 编码,与之对应
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// Entry 对象存储 ThreadLocal 对象以及与该 ThreadLocal 对象对应的值
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
}
该构造方法中会用 INITIAL_CAPACITY 属性初始化 table,该属性是个数组,且元素是 Entry 类型(ThreadLocalMap 的内部静态类)。这里不知道你是不是有疑问,我们调用 set 是只传一个值,为什么这里初始化一个数组存放数据。是因为当程序中不止一个 var1,还有 var2、var3 等多个 ThreadLocal 变量时,就需要一个数组(table)来存储不同 ThreadLocal 对象及其对应的值。
接下来分别说说 Entry 类和 ThreadLocal 类中的属性。Entry类包含两个属性,一个是 ThreadLocal 对象,另一个是需要 set 的值。ThreadLocal 中有一个属性——threadLocalHashCode,代表 ThreadLocal 对象的 hash 编码。由源码可知,threadLocalHashCode 属性是由 nextHashCode 生成,nextHashCode 是 AutomicInteger 类型,是线程安全的。
在 firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 这段代码中,用 firstKey(即 ThreadLocal 对象)的 hash 编码与 (INITIAL_CAPACITY - 1) 做按位与操作,就能生产 [0,INITIAL_CAPACITY - 1] 区间的一个数字 i, 将生成的 Entry 对象存入 table[i] 中。小结一下这里的逻辑:每个 ThreadLocal 对应一个 hash 编码,每个 hash 编码可以生成一个下标 i,将 ThreadLocal 对象与待 set 的值生成 Entry 对象,存储到 table[i] 中。这一切都是在操作当前线程的局部属性 —— threadLocals,因此跟其他线程没有关系。
当 getMap(t) 方法返回的 map 不为空,则执行 map.set(this, value); 方法,ThreadLocalMap 的 set 方法如下:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get(); // 当前位置是之前设置的,value 直接覆盖
if (k == key) {
e.value = value;
return;
} if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
} // 此时 tab[i] 为空,存储 Entry 对象
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
该方法大致处理逻辑为:首先,生成 key 对象的下标 i,生成方法与之前介绍的一样。因为不同的 ThreadLocal 都生成 [0, len-1] 区间的下标,可能会导致多个不同的 ThreadLocal 对象生成的下标 i 是一样的,产生 Hash 碰撞,也就是说两个 hash 编码抢占同一个位置,对于该例子处理 hash 碰撞的方法为开放地址法,即:从当前的位置继续向下寻找下一个位置(例子中 for 循环的 nextIndex(i, len) 语句),直到找到的位置为空,则存下当前的值。解决 Hash 碰撞的方法除了开发地址法,还有再 Hash 法和拉链法有兴趣的朋友可以自行查阅。
get 方法
理解 set 方法后,在看 get 方法就容易了,思路都是一致的。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();
} 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;
}
如果 getMap(t) 返回值为空,这说明没有初始化过,则调用 setInitialValue 方法进行初始化。否则,执行 map.getEntry(this); 语句,代码如下:
private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
ThreadLocal.ThreadLocalMap.Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
与 set 代码类似,首先生成下标 i,因为存在 Hash 碰撞,也就是说下标 i 存储的 ThreadLocal 对象不一定是方法形参 key,因此需要在 if 语句中增加 e.get() == key 判断。
小结
本篇文章介绍了 ThreadLocal 的使用及原理。每个线程设置 ThreadLocal 时,其实都是设置每个线程本地的属性,而 ThreadLocal 不存储与线程相关的变量。ThreadLocal 只提供了读写方法,这样做的好处是职责清晰,降低耦合度。有点像农夫山泉的广告语,ThreadLocal 不生产数据,只是线程的搬运工。写这篇文章的时候还感着冒,如表述不当,烦请指正。
欢迎关注公众号「渡码」,分享更多优秀书籍的笔记
ThreadLocal中优雅的数据结构如何体现农夫山泉的广告语的更多相关文章
- Python中的高级数据结构
数据结构 数据结构的概念很好理解,就是用来将数据组织在一起的结构.换句话说,数据结构是用来存储一系列关联数据的东西.在Python中有四种内建的数据结构,分别是List.Tuple.Dictionar ...
- Python中的高级数据结构详解
这篇文章主要介绍了Python中的高级数据结构详解,本文讲解了Collection.Array.Heapq.Bisect.Weakref.Copy以及Pprint这些数据结构的用法,需要的朋友可以参考 ...
- Python中的高级数据结构(转)
add by zhj: Python中的高级数据结构 数据结构 数据结构的概念很好理解,就是用来将数据组织在一起的结构.换句话说,数据结构是用来存储一系列关联数据的东西.在Python中有四种内建的数 ...
- 建议50:Python中的高级数据结构
# -*- coding:utf-8 -*- ''' Collection.Array.Heapq.Bisect.Weakref.Copy以及Pprint collections模块包含了内建类型之外 ...
- 在 Django 模板中遍历复杂数据结构的关键是句点字符
在 Django 模板中遍历复杂数据结构的关键是句点字符 ( . ). 实例二 mysit/templates/myhtml2.html修改如下 <!DOCTYPE html> <h ...
- 如何在NodeJS项目中优雅的使用ES6
如何在NodeJS项目中优雅的使用ES6 NodeJs最近的版本都开始支持ES6(ES2015)的新特性了,设置已经支持了async/await这样的更高级的特性.只是在使用的时候需要在node后面加 ...
- 如何在MyBatis中优雅的使用枚举
问题 在编码过程中,经常会遇到用某个数值来表示某种状态.类型或者阶段的情况,比如有这样一个枚举: public enum ComputerState { OPEN(10), //开启 CLOSE( ...
- Redis 中 5 种数据结构的使用场景介绍
这篇文章主要介绍了Redis中5种数据结构的使用场景介绍,本文对Redis中的5种数据类型String.Hash.List.Set.Sorted Set做了讲解,需要的朋友可以参考下 一.redis ...
- Hive 中的复合数据结构简介以及一些函数的用法说明
参见下面这篇博客: Hive 中的复合数据结构简介以及一些函数的用法说明
随机推荐
- 微信小程序之楼层效果
今天做了一个小程序实现一个楼层效果 带大家分享下经验和api的使用吧 如图 将左边和右边各分了一个组件 目录如下 其中list页面是这个楼层效果的页面 components是组成这个页面的两个组件 ...
- Jquery serialize()提交多个表单数据
ajax提交多个表单数据: 先把不同的表单分别用serialize()函数,然后把序列化后的数据用+拼接提交给后台,具体例子如下 var data1 = $('#form1).serialize(); ...
- solidity智能合约中tx.origin的正确使用场景
简介 tx.origin是Solidity的一个全局变量,它遍历整个调用栈并返回最初发送调用(或事务)的帐户的地址.在智能合约中使用此变量进行身份验证会使合约容易受到类似网络钓鱼的攻击. 但针对tx. ...
- Neo4j配置文件neo4j.conf
机器配置为256G内存,48核(物理核24)cpu,4T SAS盘(建议磁盘使用SSD) 图数据库Neo4j配置文件neo4j.conf 中常用参数: dbms.active_database=gra ...
- 1.低权限的程序向高权限的程序发消息 2.慎用setcurrentdirectory
1.低权限的程序向高权限的程序发消息 2.慎用setcurrentdirectory
- Excel催化剂100+大主题功能梳理导读
Excel催化剂历经1年4个月的开发时间,终于荣登100+个大主题功能,完成数据领域的功能大矩阵,可以说在日常的数据处理及分析上,绝大部分的共性场景已经囊括其中,是数据工作者难得一遇的优秀作品之一.因 ...
- isinstance/type/issubclass的用法,反射(hasattr,getattr,setattr,delattr)
6.23 自我总结 面向对象的高阶 1.isinstance/type/issubclass 1.type 显示对象的类,但是不会显示他的父类 2.isinstance 会显示的对象的类,也会去找对象 ...
- 百度OCR 文字识别 Android安全校验
百度OCR接口使用总结: 之前总结一下关于百度OCR文字识别接口的使用步骤(Android版本 不带包名配置 安全性弱).这边博客主要介绍,百度OCR文字识别接口,官方推荐使用方式,授权文件(安全模式 ...
- 解决tensorflow模型保存时Saver报错:TypeError: TF_SessionRun_wrapper: expected all values in input dict to be ndarray
TypeError: TF_SessionRun_wrapper: expected all values in input dict to be ndarray 对于下面的实际代码: import ...
- [leetcode]python 448. Find All Numbers Disappeared in an Array
Given an array of integers where 1 ≤ a[i] ≤ n (n = size of array), some elements appear twice and ot ...