文章目录
一、ThreadLocal 类
多线程访问同一个共享变量时,特别容易出现 并发问题,特别是 在多个线程 需要对 一个共享变量 进行 写入 时 。为了保证线程安全,一般会在访问共享变量时,进行适当的同步。
同步的措施一般是加锁,这就需要使用者对锁有一定的了解,这显然加重了使用者的负担。而 ThreadLocal 的作用:当创建一个变量后,每个线程对其进行访问的时候,访问的是自己线程的变量。
ThreadLocal 提供了线程本地变量,也就是说,如果创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程 都会有这个变量的一个 本地副本。 当多个线程操作这个变量时,实际操作的时 自己本地内存 里面的变量,从而避免了线程安全的问题。创建一个 ThreadLocal 变量后,每个线程都会复制一个变量到自己的本地内存,如图所示:
例 👀 :
public class ThreadLocalTest {
// 创建 THreadLocal 变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();
static void print(String str) {
// 打印 当前线程 本地内存中 localVariable 变量的值
System.out.println(str + ":" + localVariable.get());
// 清除 当前线程 本地内存中 localVariable 变量
localVariable.remove();
}
public static void main(String[] args) {
//创建线程 One
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
// 设置 线程One 中本地变量 localVariable 的值
localVariable.set("threadOne local variable");
// 调用打印函数
print("threadOne");
//打印本地变量值
System.out.println("threadOne remove after" + ":" + localVariable.get());
}
});
// 创建线程 Two
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
// 设置线程Two 中本地变量 localVariable 的值
localVariable.set("threadTwo local variable");
// 调用打印函数
print("threadTwo");
// 打印本地变量值
System.out.println("threadTwo remove after" + ":" + localVariable.get());
}
});
// 启动线程
threadOne.start();
threadTwo.start();
}
}
运行结果:
线程的 run 方法中 ,通过 set 方法 设置了 localVariable 的值,这其实设置的是 线程 本地内存中 的一个副本,这个副本是其他线程访问不了的。
1、ThreadLocal 相关类的类图结构
Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals,它们都是 ThreadLocalMap 类型的变量,默认情况下,每个线程中这两个变量都为 null ,只有当线程 第一次调用 ThreadLocal 的 set 或 get 方法时,才会创建它们:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
(pertaining to:有关;maintain:维护)
2、ThradLocalMap
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到, ThreadLocalMap 是 ThreadLocal 类 的 静态内部类,是一个定制化的 HashMap。(❓ 为什么是 map 结构呢❓ 因为这样的话 每个线程可以关联 多个 ThreadLocal 变量。该 map 默认的大小是16,即能存储16个键值对,超过后会扩容。)Entry 的 key 就是 ThreadLocal,而 value 就是值。同时,Entry也继承 WeakReference,所以说 Entry 所对应 key(ThreadLocal实例)的引用为一个弱引用。
每个线程的本地变量 不是存放在 ThreadLocal 实例里面的,而是存放在 调用线程的 threadLocals 变量中。 也就是说,ThreadLocal 类型的本地变量 存放在具体的线程内存空间中,ThreadLocal 就是一个工具壳,它通过 set 方法把 value 值放入调用线程的 threadLocals 中 并存放起来,当调用线程调用它的 get 方法时,再从当前线程的 threadLocals 变量中 将其拿出来使用。如果调用线程一直不终止 ,那么这个本地变量 会一直 存放在 调用线程 的 threadlocals 变量里面,所以 当不需要使用本地变量 时 ,可以通过调用 ThreadLocal 变量的 remove 方法,从当前线程的 threadLocals 中删除 该本地变量。
2、ThreadLocal 的 set 方法
public void set(T value) {
Thread t = Thread.currentThread();
//(1)
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//(2)
createMap(t, value);
}
(1):
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
getMap(t) 的作用是 获取线程自己的变量 threadLocals ,threadLocals 被绑定到了线程的成员变量上。
可以看到, set 方法 先获取当前线程 t ,然后把当前线程作为 key 去寻找对应的线程变量,找到 则设置,key 是 当前 ThreadLocal 实例对象的引用,value 就是 通过 set 方法想要设置的值,说明是第一次调用,就创建 当前线程对应的 HashMap:
(2):
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
3、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();
}
可以看到 ,get 方法也是先获取当前线程 t,调用 getMap 方法,获取当前线程的 threadLocals 变量,如果当前线程的 threadLocals 变量不为 null,则直接返回 当前线程绑定的本地变量,(也就是 threadLocals 具体的那个 entry )否则【当前线程的 threadLocals 变量为 null】,就执行 setInitialValue() 进行初始化:
private T setInitialValue() {
//(1)
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//(2)
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
这个初始化方法中,又获取一遍当前线程,然后又获取了当前线程的 threadLocals 变量 。如果 这个变量不为 null,就把 threadLocals 的 key 设置为 ThreadLocal 实例对象的引用,value 设置成 null (❓为什么需要 这两个 ”又“ 呢❓ setInitialValue()方法只在 get() 末尾被调用。 这块儿的逻辑是这样的,如果没有先 set 再 get,那么 map 为 null,调用初始化方法 setInitialValue(),获取当前线程,并创建map createMap(t, value);
;【经过 debug 验证】如果先 set 过值了,但是又 remove 了,那么在 get() 方法中,map 不为 null ,但是 e 为 null,这种情况,也是要走 return setInitialValue();
的,获取当前线程,并把 value 值设置为 null。)
(1):
protected T initialValue() {
return null;
}
4、ThreadLocal 的 remove 方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
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;
}
}
}
🎭总结:
在每个线程内部都有一个 名为 threadLocals 的成员变量,该变量的类型 是 ThreadLocalMap,其中 key 是我们定义的 ThreadLocals 变量的 this 引用,value 为我们使用 set 方法设置的值。每个线程的本地变量存放在 线程自己的内存变量 threadLocals 中,如果当前线程一直不消亡,那么这些本地变量 会一直存在,所以可能会造成内存溢出,因此 使用完毕后 要记得调用 ThreadLocal 的 remove 方法 删除对应线程的 threadLocals 的本地变量。
内存溢出 与 内存泄漏
- 内存溢出
程序运行过程中,无法申请到足够的内存,而导致的一种错误。通常发生于 OLD 段 或 Perm段 垃圾回收后 仍然无内存容纳新的 Java 对象的情况。 - 内存泄漏
存在一些被分配的对象,这些对象有下面两个特点:
(1)这些对象是可达的,即在有向图中,存在通路可以与其相连;
(2)这些对象是无用的,即程序以后不会再使用这些对象。
如果对象满足这两个条件,这些对象就可以判定为 Java 中的内存泄漏,这些对象不会被 GC 所回收,然而它却占用内存。
5、ThreadLocal 不支持继承性
例👀:
package ThreadLocalPack;
public class ThreadLocalTest2 {
//创建线程变量
public static ThreadLocal<String> threadLocal=new ThreadLocal<>();
public static void main(String[] args) {
//设置线程变量
threadLocal.set("hello");
//启动子线程
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
//子线程输出线程变量的值
System.out.println("thread:"+threadLocal.get());
}
});
thread.start();
//主线程输出线程变量的值
System.out.println("main:"+threadLocal.get());
}
}
运行结果:
可以看到,同一个 ThreadLocal 变量,在父线程(主线程)中被设置值后,在 子线程 中 是获取不到的,因为 在子线程 thread 中 调用 get() 方法时,当前线程是 thread ,而这里 调用 set 方法设置 线程变量的是 main 线程,自然 子线程访问时 返回的就是 null 了,而如果想让 子线程 能够访问到 父线程中的值,就需要:
6、InheritableThreadLocal 类
InheritableThreadLocal 类 继承自 ThreadLocal,并重写了三个方法。可以让子线程访问 在 父线程中设置的 值。它的源码:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
//(1)
protected T childValue(T parentValue) {
return parentValue;
}
//(2)
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
//(3)
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
从 (3) void createMap(Thread t, T firstValue)
方法可以看出,创建的是 当前线程的 inheritableThreadLocals 变量的实例,而不再是 threadLocals 。由 (2)ThreadLocalMap getMap(Thread t) 方法可以看出,当调用 get 获取当前线程内部的 map 变量时,获取的是 inheritableThreadLocals 而不再是 threadLocals。 也就是说,在 InheritableThreadLocal 类 中,变量 inheritableThreadLocals 代替了 threadLocals。
对 (1)childValue(T parentValue)
方法 find usages 查询无果:
在 Thread 类的源码 中 可以看到:
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
Thread 类的构造方法:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
... ...
// 获取当前线程
Thread parent = currentThread();
... ...
// 如果父线程的 inheritableThreadLocals 不为 null
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
//(1) 设置子线程中的 inheritableThreadLocals
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
(1):(👉 注意:传入的参数是 parent.inheritableThreadLocals)
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
可以看到,在 createInheritedMap 内部,使用父线程的 inheritableThreadLocals 变量作为构造函数,创建了一个新的 ThreadLocalMap ,然后在 init 方法中,赋值给了 子线程的 inheritableThreadLocals 变量。再来看看 ThreadLocalMap 的构造方法:
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
//(1)
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
可以看到,在该构造方法中,把父线程的 inheritableThreadLocals 变量的值 复制到 新的 ThreadLocalMap 中,(1)处调用的 childValue 就是 InheritableThreadLocal 类 重写的代码。
🎭总结:
InheritableThreadLocal 类 通过重写 getMap 和 createMap 方法 ,让本地变量 保存到了具体线程的 inheritableThreadLocals 变量里面,那么线程在通过 InheritableThreadLocal 类实例的 set 或者 get 方法设置变量时,就会创建当前线程的 inheritableThreadLocals 变量。当父线程创建子线程时,Thread 的构造函数会把父线程中 inheritableThreadLocals 变量中的本地变量 复制一份保存到 子线程的 inheritableThreadLocals 中。
所以把例子中的代码修改为:
public static ThreadLocal<String> threadLocal=new InheritableThreadLocal<>();
运行结果:
在什么情况下 需要子线程可以获取父线程的 threadlocal 变量呢 ?比如 子线程需要使用 存放在 threadlocal 变量中的用户登录信息;再比如 一些中间件需要把统一的 id 追的整个调用链路记录下来。
这篇博文关于 ThreadLocal 写得挺好,可以作为补充:
https://blog.csdn.net/xiaoxiaole0313/article/details/106132269
JUC 包中的 ThreadLocalRandom ,是 JDK 7 新增的,它弥补了 Random 类在多线程下的缺陷,它是借鉴 ThreadLocal 的思想实现的。
二、ThreadLocalRandom
1、Random 类及其局限性
在 JDK 7 之前,java.util.Random 是使用比较广泛的 随机数生成工具类,而且 java.lang.Math 中的随机数生成 也是使用 java.util.Random 实例。
例👀:
import java.util.Random;
public class RandomTest {
public static void main(String[] args) {
// 创建一个默认种子的 随机数生成器
Random random=new Random();
// 输出 10 个在 0~5 (包含0,不包含5)之间的随机数
for(int i=0;i<10;i++){
System.out.println(random.nextInt(5));
}
}
}
运行结果:
随机数的生成需要一个默认的种子🌱 ,这个种子是一个 long 类型的数字,可以由创建 Random 对象时,通过构造方法指定,如果不指定就会在默认构造方法的内部生成一个默认值。
- nextInt(int bound) 源码:
public int nextInt(int bound) {
// 参数检查
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
// 根据老种子生成新种子
//(1)
int r = next(31);
// 根据新种子计算随机数
int m = bound - 1;
if ((bound & m) == 0) // i.e., bound is a power of 2
r = (int)((bound * (long)r) >> 31);
else {
for (int u = r;
u - (r = u % bound) + m < 0;
u = next(31))
;
}
return r;
}
可以看出,新的 随机数的生成需要两个步骤:
(1) 首先根据老的种子生成 新的种子 。
(2)其次根据新的种子来计算新的 随机数 。
在单线程下 ,每次调用 nextInt 都是根据 老的种子 计算处新的种子,可以保证随机数产生的随机性;
而 对于多线程,当多个线程根据同一个老种子计算新种子时,第一个线程的新种子 被计算出来后,第二个线程要丢弃自己老的种子,而使用 第一个线程的新种子 来计算自己的新种子,依此类推,只有保证了这个,才能保证在多线程下产生的随机数是随机 的, Random 类中使用了 一个原子变量 AtomicLong seed 达到了这样的效果,在创建 Random 对象时 初始化的种子就被保存到了 种子变量里。
(1):
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
// 获取当前原子变量 种子的值
oldseed = seed.get();
// 根据当前种子值 计算新的种子
nextseed = (oldseed * multiplier + addend) & mask;
// CAS 操作,保证只有一个线程可以更新老的种子为新的,
//失败的种子会通过循环 重新获取更新后的种子作为当前种子 去计算
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
🎭总结:
每个 Random 实例中 都有一个 原子性的种子变量 用来记录当前的种子值。当要生成新的随机数时,需要根据当前种子 计算新的种子 并 更新回种子变量。在多线程下 使用 单个 Random 实例生成随机数时,当多个线程同时计算随机数 来计算新的种子时,多个线程会竞争同一个原子变量的更新操作,由于原子变量的更新 是 CAS 操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这会降低并发性能。
而如果每个线程都维护一个变量,则每个线程生成随机数时,都根据自己老的种子计算新的种子,并使用新种子更新老的种子,再根据新种子计算随机数。 就不会存在竞争问题啦,这会大大提高并发性能。
2、ThreadLocalRandom 类的使用
例 👀 :
public static void main(String[] args) {
// 获取一个随机数生成器
ThreadLocalRandom random=ThreadLocalRandom.current();
for(int i=0;i<10;i++)
{
// 输出 10 个在 0~5 (包含0,不包含5)之间的随机数
System.out.println(random.nextInt(5));
}
}
运行结果:
3、ThreadLocalRandom 相关类的类图结构
可以看到,ThreadLocalRandom 类 继承了 Random 类 ,并重写了 nextInt 方法,在 ThreadLocalRandom 类中并没有使用 继承自 Random 类的原子性种子变量,在 ThreadLocalRandom 类 中 并没有存放具体的种子,具体的种子是存放在 具体的调用线程的 threadLocalRandomSeed 变量里面。ThreadLocalRandom 类 类似于 ThreadLocal 类,就是个 工具类 。当线程调用 ThreadLocalRandom 类 的 current 方法时,ThreadLocalRandom 类 负责初始化 调用线程的 threadLocalRandomSeed 变量,也就是 初始化种子。
当调用 ThraeadLocalRandom 的 nextInt 方法时,实际上是 获取当前线程的 threadLocalRandomSeed 变量作为当前种子来计算新的种子,然后更新新的种子到当前线程的 threadLocalRandomSeed 变量,而后 再根据新种子 并使用具体算法 计算随机数。要注意的是 threadLocalRandomSeed 变量 就是 Thread 类里的一个普通 long 变量,并不是原子性的:(因为这个变量是线程级别的,所以不需要使用原子性)
/** The current seed for a ThreadLocalRandom */
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;
其中,seeder 和 probeGenerator 是两个原子性变量,在初始化调用线程的种子 和 探针变量 时 会用到它们,每个线程只会使用一次。
变量 instance 是 ThreadLocalRandom 的一个实例,该变量是 static 的:
/** The common ThreadLocalRandom */
static final ThreadLocalRandom instance = new ThreadLocalRandom();
当多线程通过 ThreadLocalRandom 的 current 方法获取 ThreadLocalRandom 实例时,其实 获取的是同一个实例 。但是由于具体的种子 是 存放在线程里面的,所以 ThreadLocalRandom 的实例里面只包含与线程无关的算法,所以它是线程安全的。
4、ThreadLocalRandom 主要代码逻辑
- Unsafe 机制
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;
static {
try {
// 获取 unsafe 实例
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> tk = Thread.class;
// 获取 Thread 类中 threadLocalRandomSeed 变量在 Thread 实例里的偏移量 【种子】
SEED = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSeed"));
// 获取 Thread 类中 threadLocalRandomProbe 变量在 Thread 实例里的偏移量 【探针】
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
// 获取 Thread 类中 threadLocalSecondarySeed 变量在 Thread 实例里的偏移量 【新种子】
SECONDARY = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception e) {
throw new Error(e);
}
}
- current() 方法源码:
public static ThreadLocalRandom current() {
// 默认情况下,ThreadLocalRandomProbe 的值是 0
// 如果 ThreadLocalRandomProbe 的值是 0,说明当前线程是第一次调用该方法
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
//(1) 需要初始化种子变量
localInit();
return instance;
}
(1):
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);
}
可以看出,该方法获取 ThreadLocalRandom 实例,并初始化调用线程中的 ThreadLocalRandomSeed 和 ThreadLocalRandomProbe 变量。
求 probe 时,是根据 probeGenerator 计算 当前线程中 ThreadLocalRandomProbe 的初始化值。
求 seed 时, 有个 seeder 变量,它的赋值是这样的:
private static final AtomicLong seeder = new AtomicLong(initialSeed());
进一步看 initialSeed() 方法,它是个静态方法:
private static long initialSeed() {
String pp = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"java.util.secureRandomSeed"));
if (pp != null && pp.equalsIgnoreCase("true")) {
byte[] seedBytes = java.security.SecureRandom.getSeed(8);
long s = (long)(seedBytes[0]) & 0xffL;
for (int i = 1; i < 8; ++i)
s = (s << 8) | ((long)(seedBytes[i]) & 0xffL);
return s;
}
return (mix64(System.currentTimeMillis()) ^
mix64(System.nanoTime()));
}
返回值是 System.currentTimeMillis() ,与当前时间相关,因此可以保证各线程生成的种子、新种子、随机数不同。
而后,把 ThreadLocalRandomSeed 和 ThreadLocalRandomProbe 变量 设置到当前线程。
- nextInt() 源码:
public int nextInt(int bound) {
// 参数校验
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
// 根据当前线程中的种子计算新种子
int r = mix32(nextSeed());
// 根据 新种子 和 bound 计算随机数
int m = bound - 1;
if ((bound & m) == 0) // power of two
r &= m;
else { // reject over-represented candidates
for (int u = r >>> 1;
u + m - (r = u % bound) < 0;
//(1)
u = mix32(nextSeed()) >>> 1)
;
}
return r;
}
(1):
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;
}
首先使用 r = UNSAFE.getLong(t, SEED)
获得 当前线程中 ThreadLocalRandomSeed 变量的值,然后在种子的基础上,累加 GAMMA 值作为新种子,而后使用 UNSAFE 的 putLong 方法把新种子 放入 当前线程的 ThreadLocalRandomSeed 变量中。
🎭 总结:
ThreadLocalRandom 使用 ThreadLocal 的原理,让每个线程持有一个种子变量,该种子变量只有在使用随机数时 才会被初始化。在多线程下 计算新种子时 是根据自己线程内维护的种子变量进行更新,从而避免了竞争。