使用案例,定义一个只有一个线程的线程池,提交的第一个任务为设置ThreadLocal的值,提交的第二个任务为打印第一个任务设置的值
@Test
public void test01() throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(1);
threadPool.execute(new Runnable() {
@Override
public void run() {
RequestModel requestModel = new RequestModel();
requestModel.setRequestId("1");
RequestModel.setRequestModel(requestModel);
}
});
Thread.sleep(2000);
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(RequestModel.getRequestModel().getRequestId());
}
});
Thread.sleep(10*1000);
}
public static class RequestModel {
private static final ThreadLocal<RequestModel> REQUEST_MODEL = new ThreadLocal<>();
private String requestId;
public String getRequestId() {
return requestId;
}
public void setRequestId(String requestId) {
this.requestId = requestId;
}
public static RequestModel getRequestModel() {
return REQUEST_MODEL.get();
}
public static void setRequestModel(RequestModel requestModel) {
REQUEST_MODEL.set(requestModel);
}
}
利用ThreadLocal实现线程与线程池间数据传递
@Test
public void test03() throws InterruptedException {
String token = "123";
TokenThreadLocal.setToken(token);
//业务异步执行
CUST_THREAD_POOL.execute(new AbstractRunnable() {
@Override
public void run() {
//执行线程打印
System.out.println("执行业务方法");
System.out.println("token:" + TokenThreadLocal.getToken());
}
});
Thread.sleep(2000);
CUST_THREAD_POOL.shutdown();
while (true) {
if (CUST_THREAD_POOL.isTerminated()) {
break;
}
}
System.out.println("run over");
}
private static final ThreadPoolExecutor CUST_THREAD_POOL = new ThreadPoolExecutor(4, 8,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()) {
@Override
public void execute(Runnable command) {
//提交线程数据填充到Runnable中
if (command instanceof AbstractRunnable) {
AbstractRunnable abstractRunnable = (AbstractRunnable) command;
abstractRunnable.setToken(TokenThreadLocal.getToken());
}
super.execute(command);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
//从Runnable中获取token填充到执行线程中
if (r instanceof AbstractRunnable) {
AbstractRunnable abstractRunnable = (AbstractRunnable) r;
TokenThreadLocal.setToken(abstractRunnable.getToken());
}
super.beforeExecute(t, r);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
//移除执行线程中存储的token
TokenThreadLocal.remove();
}
};
/**
* 任务虚基类
*/
private static abstract class AbstractRunnable implements Runnable {
private String token;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
/**
* token ThreadLocal操作类
*/
private static class TokenThreadLocal {
private static final ThreadLocal<String> TOKEN_THREAD_LOCAL = new ThreadLocal<>();
public static void setToken(String token) {
TOKEN_THREAD_LOCAL.set(token);
}
public static String getToken() {
return TOKEN_THREAD_LOCAL.get();
}
public static void remove() {
TOKEN_THREAD_LOCAL.remove();
}
}
源码解读
ThrealLocal实现的本质是线程内部绑定一个类似于数组的结构,每一个槽存放的是一个个继承自Entry,而这个Entry的继承自WeakReference弱引用,
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
entry的key为ThreadLocal,如果ThreadLocal置为了空,下一次执行gc时会被回收,那么key指向对象为空(详细查看https://blog.csdn.net/BtWangZhi/article/details/104274731),
每一个ThreadLocal在线程中的数组(其实如果数组的最后一个位置存放数据,是可以放到数组的头部的,应该是循环数组)中存放的位置的索引时跟ThreadLocal的threadLocalHashCode有关,
每一个ThreadLocal的threadLocalHashCode值通过原子更新类累加
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);
}
累加的数为0x61c88647转位十进制1640531527=(int) ((1L << 32) * (Math.sqrt(5) - 1)/2)=((1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1)))
=2^32*黄金比例,这样做保证了数据均匀分散。
参考https://blog.csdn.net/y4x5M0nivSrJaY3X92c/article/details/81124944
每一个entry在数组中存放的位置我们称之为槽,而槽的定位使用的hashCode与数组长度进行并运算,
int i = key.threadLocalHashCode & (table.length - 1);
如
果
该
槽
处
存
放
数
据
,
那
么
放
到
下
一
个
,
如
果
下
一
个
依
然
已
经
有
值
了
,
那
么
存
放
到
下
下
一
个
,
以
此
内
推
,
这
种
算
法
叫
线
性
探
测
法
。
\color{#FF0000}{如果该槽处存放数据,那么放到下一个,如果下一个依然已经有值了,那么存放到下下一个,以此内推,这种算法叫线性探测法。}
如果该槽处存放数据,那么放到下一个,如果下一个依然已经有值了,那么存放到下下一个,以此内推,这种算法叫线性探测法。
设置值的核心代码在静态内部类ThreadLocalMap中。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//threadLocalHashCode为ThreadLocal初始化时通过AtomicInteger原子递增后得到的数据。
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
//覆盖之前的值
e.value = value;
return;
}
if (k == null) {
//在后续的槽中找到等于key的槽,然后将找到的数据填充到槽i中。清理从i之前第一个entry的key为空至找到的entry这段entry
//如果key为空,那么将槽置为空,不为空,那么将entry移动到指定的槽或后面的槽中。expungeStaleEntry
//最后执行清理整个数组 log2 length次 ,cleanSomeSlots
replaceStaleEntry(key, value, i);
return;
}
}
//设置值
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//从staleSlot开始找已经被gc回收的Entry的key。
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
//如果在staleSlot向前不存中entry的key被回收的,那么就从i开始清理。
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//整理后续的entry
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
//slotToExpunge == staleSlot说明前置不存中已经被回收的ThreaLocal,更新回收开始的索引为当前i。
slotToExpunge = i;
}
//没有发现entry的key为当前key,那么将入参赋值到槽位空的位置。
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//发现从staleSlot开始后面,存在已经被gc回收的entry,那么执行整理。
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
主 线 逻 辑 : 利 用 T h r e a d L o c a l 传 入 的 t h r e a d L o c a l H a s h C o d e 值 与 T h r e a d 中 的 M a p 的 长 度 进 行 取 并 操 作 得 到 数 组 索 引 值 , − 》 如 果 该 槽 处 没 有 数 据 , 设 置 值 到 该 索 引 中 。 如 果 有 , 那 么 尝 试 放 到 下 下 个 位 置 , − 》 如 果 存 在 已 经 被 g c 回 收 的 E n t r y 的 k e y , 那 么 执 行 整 理 工 作 − 》 阈 值 判 断 − 》 扩 容 。 \color{#FF0000}{主线逻辑:利用ThreadLocal传入的threadLocalHashCode值与Thread中的Map的长度进行取并操作得到数组索引值,-》如果该槽处没有数据,设置值到该索引中。如果有,那么尝试放到下下个位置,-》如果存在已经被gc回收的Entry的key,那么执行整理工作-》阈值判断-》扩容。} 主线逻辑:利用ThreadLocal传入的threadLocalHashCode值与Thread中的Map的长度进行取并操作得到数组索引值,−》如果该槽处没有数据,设置值到该索引中。如果有,那么尝试放到下下个位置,−》如果存在已经被gc回收的Entry的key,那么执行整理工作−》阈值判断−》扩容。
get方法核心逻辑
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
//索引位置的数据还没被GC回收。从环形数组中继续找。
return getEntryAfterMiss(key, i, e);
}
置顶槽中为空,从槽后面继续搜索。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
//存在已经被回收的entry的key,执行整理逻辑
expungeStaleEntry(i);
else
//该处槽位已经被其他Entry占用,继续搜索下一个
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
整理数组的expungeStaleEntry需要特别说一下,好多位置都用到了,
在尝试获取数据或添加数据的时候,如果发现部分槽中entry的key为空,则说明已经被gc回收的,那么需要整理,
做了两件事,一件是如果entry的key为空,那么将槽置为空,如果不为空,那么将该槽中的entry通过并运算重新发到合适处或合适处的后面
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//清空索引staleSlot处的数据
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
//对整个环形数组中的entry数据位置进行重新放置。同时entry的k被回收的则将数组索引处置为空。
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
//数组槽位置为空
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
//将数据放到合适的后续槽位中。线性探测法。
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
关于ThreadLocal会造成内存泄漏,写一点个人的理解,
如果一个线程在执行的过程中,向ThreadLocal添加了一个大的对象,线程执行完成,但是没有执行threadLocal的remove方法,大对象挂载在线程下,线程不被GC回收,存在引用链Thread.threadLocals-》ThreadLocalMap.table-》Entry.value,虽然Entry.key为弱引用,会被回收,但是value不会这样有潜在内存泄漏的风险,不过ThreadLocal内部有一套清理机制,如果ThreadLocal置为了空,那么对应的Entry.key为活不过下一次gc,如果还有其他ThreadLocal,执行get、set、remove会整理线程下的Entry数组中Entry.key为空对应的value,置为null,让gc回收。
最
保
险
的
是
在
线
程
一
次
执
行
完
后
执
行
r
e
m
v
e
方
法
,
\color{#FF0000}{最保险的是在线程一次执行完后执行remve方法,}
最保险的是在线程一次执行完后执行remve方法,
两个线程存在父子关系,ThreadLoca就无法实现数据共享了,这个时候就需要InheritableThreadLocal
@Test
public void test02() throws InterruptedException {
InheritableThreadLocal<String> threadLocal=new InheritableThreadLocal<>();
threadLocal.set("1");
new Thread(){
@Override
public void run() {
System.out.println(threadLocal.get());
}
}.start();
Thread.sleep(5000);
System.out.println("run over");
}
子线程也共享了父线程的中的数据,
new 子线程的时候,把父线程的inheritableThreadLocals复制给子线程而已。代码在Thread-》init中。这样就得到了父线程中的数据。
参考:
https://www.cnblogs.com/micrari/p/6790229.html
https://blog.csdn.net/zzg1229059735/article/details/82715741
关于threadLocalHashCode值累加参考:
https://blog.csdn.net/y4x5M0nivSrJaY3X92c/article/details/81124944
Random 生成随机数
int min = 0;
int max = 100;
Random random = new Random();
int s = random.nextInt(max) % (max - min + 1) + min;
System.out.println(s);
Random初始时根据当前时间生成一个其实随机数即种子,
后面一直再上面进行计算然后返回,同时会计算下一次的种子,通过CAS算法存放到AtomicLong类型的变量中,在并发的场景下会出现频繁自旋。
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
ThreadLocalRandom
和Random一样,初始化的时候会生成一个种子,通过UnSafe设置到Thread中,
static final void localInit() {
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == 0) ? 1 : p; // skip 0
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
UNSAFE.putLong(t, SEED, seed);
UNSAFE.putInt(t, PROBE, probe);
}
后面每次生成随机数都生成下一个种子存放到Thread的threadLocalRandomSeed变量中。
final long nextSeed() {
Thread t; long r; // read and update per-thread seed
UNSAFE.putLong(t = Thread.currentThread(), SEED,
r = UNSAFE.getLong(t, SEED) + GAMMA);
return r;
}
参考:https://mp.weixin.qq.com/s/Ke1iZ3Ec2wf06BfqrNxbJQ