一.什么是ThreadLocal
ThreadLocal的作用是提供线程内的局部变量,这种变量在多线程环境下访问时能够保证各个线程里变量的独立性。ThreadLocal 不是一个线程,而是一个线程的本地化对象。当某个变量在使用 ThreadLocal 进行维护时,时ThreadLocal 为使用该变量的每个线程分配了一个独立的变量副本。每个线程可以自行操作自己对应的变量副本,而不会影响其他线程的变量副本。
二.Threadlocal 使用场景
/**
* Copyright (C), 2015-2019, XXX有限公司
* FileName: MainTest
* Author: zhaoyu
* Date: 2019-10-21 17:51
* Description: 测试为什么使用ThreadLocal
* History:
* <author> <time> <version> <desc>
* 作者姓名 修改时间 版本号 描述
*/
package com.example.demo.Dao;
import java.util.function.Supplier;
public class MainTest {
public static void main(String[] args) {
Bank1 bank1 =new Bank1();
Thread thread1 = new Thread(()-> bank1.deposit(100),"小红");
Thread thread2 = new Thread(()-> bank1.deposit(200), "小刚");
Thread thread3 = new Thread(() -> bank1.deposit(200), "小张");
thread1.start();
thread2.start();
thread3.start();
}
}
class Bank1{
private int money = 1000;
public void deposit(int money){
//获取当前线程的名字
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "--当前账户余额为:" + this.money);
this.money += money;
System.out.println(threadName + "--存入 " + money + " 后账户余额为:" + this.money);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:存在多线程输出结果混乱
小红--当前账户余额为:1000
小刚--当前账户余额为:1000
小张--当前账户余额为:1000
小刚--存入 200 后账户余额为:1300
小红--存入 100 后账户余额为:1100
小张--存入 200 后账户余额为:1500
以上的例子是模拟多线程获取方法的局部变量并对其修改,最后程序输出的结果可见结果混乱。
使用 ThreadLocal 保存对象的局部变量。
/**
* Copyright (C), 2015-2019, XXX有限公司
* FileName: MianTest_loca
* Author: zhaoyu
* Date: 2019-10-21 18:20
* Description: 测试ThreadLoca对比
* History:
* <author> <time> <version> <desc>
* 作者姓名 修改时间 版本号 描述
*/
package com.example.demo.Dao;
import java.util.function.Supplier;
public class MianTest_loca {
public static void main(String[] args) {
Bank bank = new Bank();
Thread xMThread = new Thread(() -> bank.deposit(200), "小明");
Thread xGThread = new Thread(() -> bank.deposit(100), "小刚");
Thread xHThread = new Thread(() -> bank.deposit(200), "小红");
xMThread.start();
xGThread.start();
xHThread.start();
}
}
class Bank {
// 初始化账户余额为 1000
ThreadLocal<Integer> account = ThreadLocal.withInitial(new Supplier<Integer>() {
@Override
public Integer get() {
return 1000;
}
});
public void deposit(int money) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "--当前账户余额为:" + account.get());
account.set(account.get() + money);
System.out.println(threadName + "--存入 " + money + " 后账户余额为:" + account.get());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果为:
小明--当前账户余额为:1000
小明--存入 200 后账户余额为:1200
小红--当前账户余额为:1000
小红--存入 200 后账户余额为:1200
小刚--当前账户余额为:1000
小刚--存入 100 后账户余额为:1100
这次的这个例子 用ThreadLocal 维护线程的局部变量
三.Threadlocal API 方法
1)protected T initialValue()
返回当前线程的局部变量副本的变量初始值
2)T get()
返回当前线程的局部变量副本的变量值,如果此变量副本不存在,则通过 initialValue() 方法创建此副本并返回初始值。
3)void set(T value)
设置当前线程的局部变量副本的变量值为指定值。
4)void remove()
删除当前线程的局部变量副本的变量值。
在实际使用中,我们一般都要重写 initialValue() 方法,设置一个特定的初始值。
initialValue的初始化方法
//new ThreadLocal方式
final ThreadLocal<String> commandThreads = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "execute :"+System.currentTimeMillis();
}
};
System.out.println(commandThreads.get());
//withInitial方式:
ThreadLocal<String> commandThreadnew =
// ThreadLocal.withInitial(()-> "execute :"+System.currentTimeMillis());
ThreadLocal.withInitial(()->new String("execute :"+System.currentTimeMillis()));
System.out.println(commandThreadnew.get());
(//new Supplier<String>(){}方式
ThreadLocal<String> commandThreadnew1 =
ThreadLocal.withInitial(new Supplier<String>() {
@Override
public String get() {
return "execute :"+System.currentTimeMillis();
}
});
System.out.println( commandThreadnew1.get());
四.Threadlocal 实现思路
Tread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,我们使用线程的时候有一个自己的ThreadLocalMap。ThreadLocalMap有自己的独立实现,可以简单认为ThreadLocal视为key,value为代码中放入的值(实际上key并不是ThreadLocal本身,通过源码可以知道它是一个弱引用)。调用ThreadLocal的set方法时候,都会存到ThreadLocalMap里面。调用ThreadLocal的get方法时候,在自己map里面找key,从而实现线程隔离。
5.Threadlocal 源码分析
ThreadLocal最主要的实现在于ThreadLocalMap这个内部类里面,我们重点关注ThreadLocalMap这个类的用法。
ThreadLocalMap提供了一种为ThreadLocal定制的高效实现,并且自带一种基于弱引用的垃圾清理机制。
1.存储结构方面
存储结构可以理解为一个map,但是不要和java.util.Map弄混。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
从代码里可以看出Entry 就是ThreadLocalMap 的一个节点,继承了WeakReference类定义了一个类型为Object的value,用于存放塞到ThreadLocal里的值,key可以视为为ThreadLocal。
问题1.为什么要用弱引用,因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java四种引用的的第三种(其它三种强引用、软引用、虚引用),比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。
2.Entry 里面的成员变量和方法
/**
* The initial capacity -- MUST be a power of two.
* 初始容量,必须为2的幂数
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
* 根据需要调整大小必须为2的幂数
*/
private Entry[] table;
/**
* The number of entries in the table.
* Entry中条目的数量
*/
private int size = 0;
/**
* The next size value at which to resize.
* 要调整大小的下一个大小值。默认为0
*/
private int threshold; // Default to 0
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.将调整大小阈值设置维持最坏2/3的负载因子
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* Increment i modulo len.
* 上一个索引
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
* 下一个索引
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
由于ThreadLocalMap使用线性探测法来解决散列冲突,所以实际上Entry[]数组在程序逻辑上是作为一个环形存在的。
构造函数:
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
* 构造一个最初包含(firstKey, firstValue)的新映射 threadlocalmap是延迟构造的,因 此当我们至少有一个元素可以放进去的时候才去创建。
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
重点说下这个hash函数int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);ThreadLocal类中有一个被final修饰的类型为int的threadLocalHashCode,它在该ThreadLocal被构造的时候就会生成,相当于一个ThreadLocal的ID,而它的值来源于
private final int threadLocalHashCode = nextHashCode();
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
* 连续生成的哈希码之间的区别——循环隐式顺序线程本地id以近乎最优的方式展开
* 用于两倍大小表的乘法哈希值。
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
* 下一个hash值
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
通过理论和实践算出通,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。
ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。这就回答了上文抛出的为什么大小要为2的幂的问题。为了优化效率。对于& (INITIAL_CAPACITY - 1),相信有过算法阅读源码较多的程序员,一看就明白,对于2的幂作为模数取模,可以用&(2n-1)来替代%2n,位运算比取模效率高很多。至于为什么,因为对2^n取模,只要不是低n位对结果的贡献显然都是0,会影响结果的只能是低n位。(此处摘录其他文章,不太理解,以后、、、)
3.ThreadLocal 中的get方法
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//通过当前线程获取 ThreadLocalMap
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();
}
ThreadLocal 中的 get方法实际是获取了 ThreadLocalMap 中的 Entry ,从Entry中获取值
4.ThreadLocal 中的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);
}
5.ThreadLocal中的remove方法调用TreadLocalMap中的remove
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}