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 中的复合数据结构简介以及一些函数的用法说明
随机推荐
- 对DatagramSocket的使用实例(java使用UDP进行数据传输)
今天刚看懂的一点点东西,记录一下,方便自己回顾 客户端: Client.java import java.io.IOException; import java.net.DatagramPacket; ...
- C#窗体实现打开关闭VM虚拟机
vixclass.cs//定义开机.关机等函数 using System; using System.Collections.Generic; using System.Linq; using Sys ...
- Vue匿名组件使用keep-alive后动态清除缓存
在使用Vue开发管理系统项目的时候,为了保存页面的浏览状态,我们可以使用内置组件keep-alive来缓存组件内部状态,避免重新渲染. <keep-alive> <router-vi ...
- Excel催化剂开源第28波-调用Google规划求解库
在Excel催化剂的自定义函数中,有规划求解的函数,用于在一些凑数的场景,某财务工作网友向我提出的需求,例如用于凑发票额使用. 一般开发票的场景是多次采购合在一起开具,即多个订单产生后开,同时发票一般 ...
- Tiny Counting
也许更好的阅读体验 样例一 输入 4 1 4 3 2 输出 3 样例二 输入 5 9 1 0 0 5 输出 8 题解 这是本人自己想了2个半小时才想出来的方法,稍稍有点复杂但是很好理解 题目的意思就是 ...
- android开发--使用webView加载tel协议不会打开拨号盘解决
在加载url之前进行判断,url是否是tel协议开头,然后进行加载,即可打开拨号盘 mWebView.setWebViewClient(new WebViewClient() { @Override ...
- python执行unittest界面设置
执行单元测试时,系统会自动添加unittest in...的执行服务器. 执行时unittest in...的执行服务器在界面右上方可以看到,且执行结果为左侧框和右侧统计结果. 如果没有,会导致测试结 ...
- codemirror使用
JS使用 使用bower下载 bower i codemirror 引入样式文件 <link rel="stylesheet" type="text/css&quo ...
- 如何在 Centos7 中安装 gcc
系统环境:Centos7.4 今天在安装 Nodejs8.7 的时候,报了一个警告: WARNING: C++ Compiler too old, need g++ 4.9.4 or clang++ ...
- PHP与ECMAScript_3_常用字符串函数
PHP ECMAScript 长度 strlen($str) str.length 查找类 $str[n] ...