本篇文章主要讲解 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. 在 Microsoft.VisualStudio.Setup.Engine.Install(Product product, String destination, CancellationToken token)无法在相同位置或现有实例“20cc4971”的子目录上安装指定实例“ebc82a8e”的解决方案

    在所在的安装目录根目录下搜索实例 如 20cc4971 将文件夹全部删除. 一般默认安装在C盘,所以在C盘搜索实例文件夹,将其全部删除.

  2. iPhone调试移动端webview

    一.模拟器调试 1.启动Xcode 2.选择菜单Xcode - Open Developer Tool - Simulator 3.启动Simulator后,选择Simulator菜单Hardware ...

  3. 最全caffe安装踩坑记录(Anaconda,nvidia-docker,Linux编译)

    Anaconda,nvidia-docker,Linux三种方式安装caffe 1.Anaconda安装caffe 1.首先安装anaconda 2.创建虚拟环境(python2.7) conda c ...

  4. P2822组合数问题

    组合数问题(NOIP2016提高组Day2T1) Time Limit:1000MS  Memory Limit:512000K [题目描述] 组合数表示的是从n个物品中选出m个物品的方案数.举个例子 ...

  5. [HAOI2006]聪明的猴子 题解

    题意: 在一个热带雨林中生存着一群猴子,它们以树上的果子为生.昨天下了一场大雨,现在雨过天晴,但整个雨林的地表还是被大水淹没着,部分植物的树冠露在水面上.猴子不会游泳,但跳跃能力比较强,它们仍然可以在 ...

  6. delegate委托的例子,实现对Form中控件的更新

    using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; usin ...

  7. Excel催化剂开源第49波-Excel与PowerBIDeskTop互通互联之第三篇

    在PowerBIDeskTop开启的SSAS服务,和Sqlserver所开启的一个本质的区别是,前者其端口号是随机生成的,即上一次打开获得的端口号,下一次关闭后再打开,系统分配给它新的端口号,而后者因 ...

  8. 【Java中级】(四)多线程

    线程的概念 进程和线程的主要差别在于它们是不同的操作系统资源管理方式.进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径.线程有自己的堆栈和局 ...

  9. 安装解压版MySQL5.76及以上版本 出现服务正在启动-服务无法启动的问题

     最近重装了系统,去MySQL官网下载了最新的MySQL5.7.9,我选择的是解压版,安装之后启动服务的时候,提示服务无法启动,在网上找了很多教程,弄了很久都没有弄好,后来还是决定去英文官网找找答案, ...

  10. JavaWeb学习笔记—监听器

    监听器Listener是JavaWeb中的三大组件之一 按监听的对象划分,可以分为 ServletContext对象监听器 HttpSession对象监听器 ServletRequest对象监听器 按 ...