本篇文章主要讲解 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中优雅的数据结构如何体现农夫山泉的广告语的更多相关文章

  1. Python中的高级数据结构

    数据结构 数据结构的概念很好理解,就是用来将数据组织在一起的结构.换句话说,数据结构是用来存储一系列关联数据的东西.在Python中有四种内建的数据结构,分别是List.Tuple.Dictionar ...

  2. Python中的高级数据结构详解

    这篇文章主要介绍了Python中的高级数据结构详解,本文讲解了Collection.Array.Heapq.Bisect.Weakref.Copy以及Pprint这些数据结构的用法,需要的朋友可以参考 ...

  3. Python中的高级数据结构(转)

    add by zhj: Python中的高级数据结构 数据结构 数据结构的概念很好理解,就是用来将数据组织在一起的结构.换句话说,数据结构是用来存储一系列关联数据的东西.在Python中有四种内建的数 ...

  4. 建议50:Python中的高级数据结构

    # -*- coding:utf-8 -*- ''' Collection.Array.Heapq.Bisect.Weakref.Copy以及Pprint collections模块包含了内建类型之外 ...

  5. 在 Django 模板中遍历复杂数据结构的关键是句点字符

    在 Django 模板中遍历复杂数据结构的关键是句点字符 ( . ). 实例二 mysit/templates/myhtml2.html修改如下 <!DOCTYPE html> <h ...

  6. 如何在NodeJS项目中优雅的使用ES6

    如何在NodeJS项目中优雅的使用ES6 NodeJs最近的版本都开始支持ES6(ES2015)的新特性了,设置已经支持了async/await这样的更高级的特性.只是在使用的时候需要在node后面加 ...

  7. 如何在MyBatis中优雅的使用枚举

    问题 在编码过程中,经常会遇到用某个数值来表示某种状态.类型或者阶段的情况,比如有这样一个枚举:   public enum ComputerState { OPEN(10), //开启 CLOSE( ...

  8. Redis 中 5 种数据结构的使用场景介绍

    这篇文章主要介绍了Redis中5种数据结构的使用场景介绍,本文对Redis中的5种数据类型String.Hash.List.Set.Sorted Set做了讲解,需要的朋友可以参考下 一.redis ...

  9. Hive 中的复合数据结构简介以及一些函数的用法说明

    参见下面这篇博客: Hive 中的复合数据结构简介以及一些函数的用法说明

随机推荐

  1. 查看http请求的header信息

    1 下载chrome浏览器 chrome浏览器是google开发的一块非常绑定浏览器.chrome浏览器下载地址. 2 通过chrome控制台查看http请求的header信息 2.1 打开chrom ...

  2. C#窗体实现打开关闭VM虚拟机

    vixclass.cs//定义开机.关机等函数 using System; using System.Collections.Generic; using System.Linq; using Sys ...

  3. FFT中的一个常见小问题(递推式)

    FFT中的一个常见小问题这里不细说FFT的内容,详细内容看这些就足以了解大概了小学生都能看懂的FFT!!!FFT详解补充——FFT中的二进制翻转问题主要是对学习过程中一个容易困扰的小问题进行解释,以便 ...

  4. websocket的加密和解密过程

    加密: import struct msg_bytes = "the emperor has not been half-baked in the early days of the col ...

  5. .net持续集成sonarqube篇之sonarqube基本操作(二)

    系列目录 Activity界面操作 Activity界面主要是对多次构建管理界面,主要是帮助管理员快速了解项目每次构建与以往构建相比问题是增加了还是减少了等指标.由于目前我们仅进行了一次构建,因此没有 ...

  6. Linux目录文件

    /binbin是binary的缩写.这个目录沿袭了UNIX系统的结构,存放着使用者最经常使用的命令.例如cp.ls.cat,等等. /boot这里存放的是启动Linux时使用的一些核心文件. /dev ...

  7. [Revit]Autodesk Revit 二次开发整理(资料、准备工作和环境搭建)

    1 前言 Revit被Autodesk收购之后,整理和开放了一大部分API,供开发者实现自己的功能和程序,总体来说API的功能比较完善,毕竟市面上已经出现了各式各样的插件. 本人也是初学者,在Revi ...

  8. 上传及下载github项目

    1.上传本地项目 git init //把这个目录变成Git可以管理的仓库         git add README.md //文件添加到仓库         git add . //不但可以跟单 ...

  9. 【MySQL】日常小技巧汇总,更新中……

    创建表时修改自增主键,添加 AUTO_INCREMENT=<Number> ,例如: CREATE TABLE `table_name` ( `id` int(11) unsigned N ...

  10. 【iOS】获取项目名和版本号

    iOS 开发中,有时候需要获取项目名和版本号,示例代码如下: -(void)getProjectNameAndVersion{ appName = [[[NSBundle mainBundle] in ...