亲自动手实现一个Threadlocal

局部-全局和ThreadLocal变量

我们经常使用局部变量和全局变量。

局部变量声明在某个方法中或代码块中,随着代码块的结束因没有引用而被回收,如下图 tl4.png

局部变量是线程独享的,生命周期仅存在于该方法块中

全局变量包括静态变量和对象的成员变量,是所有线程都可以访问的,如下图:

tl3.png

但线程读写同一个全局变量,需要解决并发问题

还有一种变量,称为ThreadLocal变量。 为什么会有threadlocal?因为有些线程需要保存当前线程相关的信息,比如请求信息,用户信息,session等。对于每一个线程这个变量有自己不同的值,它不是代码块的局部变量,也不是全局变量。ThreadLocal变量是线程私有的,线程之间使用相互不影响。这就是ThreadLocal,将变量的使用范围恰当的保存到了全局变量和局部变量之间。如下图:

tl6.png Threadlocal可以:

  • 不用考虑并发安全问题
  • 像访问局部变量一样访问全局的变量

那能做到这样的原因是什么,本文将通过亲自动手实现一个Threadlocal来理解其内幕。

demo-ThreadLocal基本工作方式

下面是一个ThreadLocal应用的典型场景:

tl1.png 代码如下:

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两个变量引用,如下:

tl2.png 两种场景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也不是那么简单,要考虑哈希表的冲突,扩容等问题。

在实现的过程中,我们虽然理解了弱引用在这里的作用,但是还有一个问题没有解答清楚,也就是我们从弱引用开始引入实现却没说明白的问题,下一章将理清这个问题。

可能引起的问题

因为只要这个哈希表不回收,这个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的原理、使用总结如下:

cl8.png

Guess you like

Origin juejin.im/post/7053831988603519013