亲自动手实现一个ThreadLocal
局部-全局和ThreadLocal变量
我们经常使用局部变量和全局变量。
局部变量声明在某个方法中或代码块中,随着代码块的结束因没有引用而被回收,如下图
局部变量是线程独享的,生命周期仅存在于该方法块中。
全局变量包括静态变量和对象的成员变量,是所有线程都可以访问的,如下图:
但线程读写同一个全局变量,需要解决并发问题。
还有一种变量,称为ThreadLocal变量。 为什么会有threadlocal?因为有些线程需要保存当前线程相关的信息,比如请求信息,用户信息,session等。对于每一个线程这个变量有自己不同的值,它不是代码块的局部变量,也不是全局变量。ThreadLocal变量是线程私有的,线程之间使用相互不影响。这就是ThreadLocal,将变量的使用范围恰当的保存到了全局变量和局部变量之间。如下图:
Threadlocal可以:
- 不用考虑并发安全问题
- 像访问局部变量一样访问全局的变量 那能做到这样的原因是什么,本文将通过亲自动手实现一个Threadlocal来理解其内幕。
demo-ThreadLocal基本工作方式
下面是一个ThreadLocal应用的典型场景:
代码如下:
package com.example.demo;
public class ThreadLocalTest {
public static void main(String[] args) {
for (int i = 1; i < 4; i++) {
Handler handler = new Handler("user" + i);
handler.start();
}
}
}
class Request{
String user;
public void setUser(String user) {
this.user = user;
}
public Request(String user) {
this.user = user;
}
}
class Handler extends Thread{
String user;
public Handler(String user) {
this.user = user;
}
ThreadLocal<Request> request =new ThreadLocal() ;
@Override
public void run() {
request.set(new Request(user));
Random random = new Random(400);
try {
Thread.sleep(random.nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": " + request.get().user);
}
}
输出
Thread-1: user2
Thread-0: user1
Thread-2: user3
3个线程都使用了同一个变量ThreadLocal request,而且设置和获取的值是对应的,像用局部变量一样使用全局变量了。各个线程使用request就像使用各个线程的局部变量一样,没有线程安全问题。
这是因为这个变量确实存到了线程对象的一个成员变量里面去,每个thread都有一个ThreadLocalMap类型的变量来存放这个ThreadLocal变量和对应的要set的值(包装成如下的Entry对象)
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
WeakReference<ThreadLocal<?>> 表示Entry的key是一个弱引用,key弱引用类型是ThreadLocal对象,value可以是任意Object对象。
亲自动手实现Threadlocal
demo看起来很简单,跳进去看Threadlocal的源码也不难。但总觉得差那么点意思,下面将从弱引用原理和实验谈起,直到亲自动手实现一个Threadlocal。
从key的回收♻️聊起
为啥要从弱引用key的回收聊起?因为这个是ThreadLocal原理里面最饶舌的一部分了,后面将解释具体的原因,可以跟着我一步一步的从这里看起。
弱引用GC回收
只要发生full gc,WeakReference引用的对象都会被释放。
@Test
void test(){
WeakReference<byte[]> weakReference = new WeakReference<byte[]>(new byte[1]);
System.out.println(weakReference.get());
System.gc();
System.out.println(weakReference.get());
Assertions.assertTrue(weakReference.get()==null);
}
[B@1786f9d5
null
所以在发生gc之后,原来在堆上面分配的字节数组对象new byte[1]
就会被回收了。
但是如果还有强引用指向这个对象则不会被释放,如下demo
@Test
void test(){
byte[] bytes = new byte[1];
WeakReference<byte[]> weakReference = new WeakReference<byte[]>(bytes);
System.out.println(weakReference.get());
System.gc();
System.out.println(weakReference.get());
Assertions.assertTrue(weakReference.get()==null);
}
[B@1786f9d5
[B@1786f9d5
org.opentest4j.AssertionFailedError:
Expected :true
Actual :false
这里的GC roots有bytes和weakReference两个变量引用,如下:
两种场景GC的区别:
- 上面场景只有weakReference变量,弱引用可清理
- 下面场景还有bytes变量,是强引用不可清理
开始动手:继承WeakReference的Entry
了解了WeakReference之后就可以开始动手了,如下:
public class ReferenceTest {
@Test
void test(){
Entry[] table=new Entry[16];
for (int i = 0; i < 3; i++) {
table[i]=new Entry(new byte[1],"hello"+i);
}
System.gc();//gc的时候,这些Entry也都会被清理
for (int i = 0; i < 3; i++) {
System.out.println("entry key = " + table[i].get());
}
}
}
class Entry extends WeakReference<byte[]> {
Object value;
public Entry(byte[] referent, Object value) {
super(referent);
this.value = value;
}
}
entry = null
entry = null
entry = null
- 将上面的字节数组对象替换为Entry
- Entry继承WeakReference<byte[]>,还有一个成员value,Entry是模拟ThreadLocalMap的数组对象类型。
- 同理,gc的时候,这些Entry也都会被清理
更进一步:弱引用的Threadlocal
进一步模拟Entry,继承WeakReference<ThreadLocal<?>>
,如下
public class ReferenceTest {
@Test
void test(){
Entry[] table=new Entry[16];
for (int i = 0; i < 3; i++) {
Request request = new Request("user" + i);
ThreadLocal<Request> threadLocal = new ThreadLocal<>();
table[i]=new Entry(threadLocal,request);//相当于调用ThreadLocal.set方法
}
for (int i = 0; i < 3; i++) {
System.out.println("entry key = " + table[i].get());
System.out.println("value = " + table[i].value);
}
System.gc();//gc的时候,这些WeakReference变量都会被清理吧;
for (int i = 0; i < 3; i++) {
System.out.println("entry key = " + table[i].get());
System.out.println("value = " + table[i].value);
}
}
}
class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
public Entry(ThreadLocal<?> referent, Object value) {
super(referent);
this.value = value;
}
}
结果
entry key = java.lang.ThreadLocal@1786f9d5
value = com.example.demo.Request@704d6e83
entry key = java.lang.ThreadLocal@43a0cee9
value = com.example.demo.Request@eb21112
entry key = java.lang.ThreadLocal@2eda0940
value = com.example.demo.Request@3578436e
entry key = null
value = com.example.demo.Request@704d6e83
entry key = null
value = com.example.demo.Request@eb21112
entry key = null
value = com.example.demo.Request@3578436e
虽然只是简短的几行代码,但几乎已经模拟实现了一个ThreadLocal和ThreadLocalMap。
是的!ThreadLocal其实就是这么简单。
继续:key和下标的转化
上面的实现还存在一个问题,放进去的key在哪个位置,哪个下标呢?
hashacode
如果每个threadlocal都有唯一的hashcode,那么在设置的时候,就可以按照key和下标对应的去查找了。
ThreadLocal是使用静态方法计算hashcode的。
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.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
不仅保证每一个ThreadLocal对象的hashacode不冲突,还能保证尽量减少到下标key.threadLocalHashCode & (len-1)
的冲突。
用数组实现的hash表ThreadlocalMap
给MyThreadLocal加上hashcode的实现,ThreadLocalMap实现具体的get和set方法,如下:
class ThreadMock{
MyThreadLocal.ThreadLocalMap threadLocals;
public ThreadMock(MyThreadLocal.ThreadLocalMap threadLocals) {
this.threadLocals = threadLocals;
}
}
class MyThreadLocal<T> extends ThreadLocal{
ThreadMock threadMock;
public MyThreadLocal(ThreadMock threadMock) {
this.threadMock= threadMock;
}
static class ThreadLocalMap{
public ThreadLocalMap() {
}
int len=16;
Entry[] table=new Entry[len];
Object get(MyThreadLocal<?> key){
int i = key.threadLocalHashCode & (len-1);
if (table[i]!=null){
if (table[i].get()==key) {
return table[i].value;
}else {
//则可能被其他的给替换了,导致
}
}
return null;
}
public void set(MyThreadLocal<?> key,Object value){
int i = key.threadLocalHashCode & (len-1);
table[i]=new Entry(key,value);
}
}
public Object get(){
return threadMock.threadLocals.get(this);
}
public void set(Object value){
threadMock.threadLocals.set(this,value);
}
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);
}
}
class Entry extends WeakReference<MyThreadLocal<?>> {
Object value;
public Entry(MyThreadLocal<?> referent, Object value) {
super(referent);
this.value = value;
}
}
测试:
@Test
void test(){
ThreadMock threadMock= new ThreadMock(new MyThreadLocal.ThreadLocalMap());
for (int i = 0; i < 5; i++) {
Request request = new Request("user" + i);
MyThreadLocal<Request> threadLocal = new MyThreadLocal<>(threadMock);
threadLocal.set(request);
}
System.out.println("threadMock = " + threadMock);
}
- ThreadLocalMap里面生成的下标依次是0,7,14,5,12
但现在的缺点是仍旧有冲突,多试几次,则会出现相同的key:
0,7,14,5,12 3 10 1 8 15 6 13 4 11 2 9 **0** 7
。
第17次的时候,下标是0,开始冲突了。
所以还要解决冲突的问题。
hashmap冲突解决有多种方法,在ThreadLocal里面使用拉链法解决冲突。
拉链法解决哈希表冲突
get改写:
Object get(MyThreadLocal<?> key){
int i = key.threadLocalHashCode & (len-1);
Entry entry = table[i];
if (entry !=null && entry.get()==key){
return entry.value;
}
return getEntryAfterMiss(key,i,entry);
}
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)
//被垃圾回收了的情况。。开始清理k,以及后面的一些值
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清理value
tab[staleSlot].value = null;
//清理这个entry
tab[staleSlot] = null;
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {//后面的entry key也被gc了
e.value = null;
tab[i] = null;
size--;
} else {//entry key存在
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {//不是刚好应该放在i这个槽
tab[i] = null;
while (tab[h] != null)//重新找一个离h最近的位置
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
同样set也要做一些冲突的处理和null值的清理工作。
扩容
如果存储的数据超过了一个阈值,还要考虑给哈希表扩容。
实现的总结
实现一个ThreadLocal其实还挺简单,因为是Thread Local变量,get、set和扩容等操作的时候也不用考虑并发加锁等难题。但是要实现一个完备的ThreadLocal也不是那么简单,要考虑哈希表的冲突,扩容等问题。
在实现的过程中,我们虽然理解了弱引用在这里的作用,但是还有一个问题没有解答清楚,也就是我们从弱引用开始引入实现却没说明白的问题,下一章将理清这个问题。
可能引起的问题
因为系统多采用线程池,线程往往是长期存在的。则线程的threadLocal哈希表也长期存在。只要这个哈希表不回收,这个key也永远不会被回收,为了防止内存泄露,所以使用弱引用来指向key。
但是value为何不这样做呢?而我们常说的ThreadLocal的内存泄露问题就是因为value引起的。
为何value不使用弱引用
既然key都使用了,为什么value的使用不使用弱引用呢?假设也给value用弱饮用, 如下demo:
@Test
void test(){
ThreadMock threadMock= new ThreadMock(new MyThreadLocal.ThreadLocalMap());
for (int i = 0; i < 5; i++) {
MyThreadLocal<Request> threadLocal = new MyThreadLocal<>(threadMock);
threadLocal.set(new WeakReference<>(new Request("user" + i)));
}
System.out.println("threadMock = " + threadMock);
System.gc();
System.out.println("threadMock = " + threadMock);
}
-
debug发现,因为已经没有其他引用,这样做
threadLocal.set(new WeakReference<>(new Request("user" + i)));
,gc后就回收了value。 -
gc回收了value,造成了误杀。
-
value不那么设置的原因是因为很可能没有其他引用了,所以value通常的用法是不用WeakReference。 value可能的内存泄露问题和随便回收(丢失数据)比起来,反而不是什么事。
那么,这个将会使得value的内存长期存在于heap区(如果没有被get和set清理掉),永远不会被回收,这就引起了内存泄漏,所以养成良好的remove习惯将会对此有所帮助。
value的回收:remove函数
为了防止value引起的内存释放问题,可以调用remove函数,手动释放value.
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
其实就是:
if(table[i].get()==null){
table[i].value=null;
}
key为何使用弱引用
有点绕回去了,但还是要问:为何key还是坚持要使用弱引用呢? 这是因为用法的问题。 ThreadLocal变量的经典用法就是像上文提到的demo那样,声明一个类变量或静态变量指向这个对象,
demo中因为Handler类成员引用了这个ThreadLocal,所以即时中间发生了gc,也不会被gc释放。 只有当这个Handler对象被释放或者静态类unload才会出现gc,这往往也是符合预期的。
问题总结
key和value的原理、使用总结如下:
转载自:https://juejin.cn/post/7053831988603519013