美文网首页
ThreadLocal

ThreadLocal

作者: 程序员学徒 | 来源:发表于2020-05-21 21:41 被阅读0次

一、ThreadLocal简介

ThreadLocal是在并发编程的多线程环境下用来保证变量线程安全的工具类,其基本使用方法如下:

public class Main {

    static ThreadLocal<Integer> tl = new ThreadLocal<>();

    public static void main(String[] args) {
        
        // 创建两个线程,一个线程在1秒后写入值,一个线程在2秒后读出,观察结果
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 暂停1秒
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 设置变量的值为100
                tl.set(100);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 暂停2秒
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 获取变量的值
                Integer integer = tl.get();
                System.out.println(integer);
            }
        }).start();
    }

}

// 输出
null

观察结果可以发现,在第二个线程中并不能获取在第一个线程中设置的值。下面将从ThreadLocal的源码分析,它是如何实现线程安全的。


二、ThreadLocal源码分析

2.1 构造函数

构造函数中什么都没有

public ThreadLocal() {
}

2.2 set()

  • set()
// ThreadLocal类中的set()
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 从当前线程中获取map,注意map是定义在Thread类中的对象
    ThreadLocalMap map = getMap(t);
    // key为this对象,也就是当前的threadlocal对象
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

用过ThreadLocal的人都知道它是使用一个map结构来保证线程安全性的,但这个map是保存在哪里,key和value分别是什么?在set()方法中就可以看出来:

  1. 获取当前Thread对象
  2. 通过当前Thread对象获取map
  3. 向map中添加key-value

下面我们来看getMap(t)方法:

// ThreadLocal类中的getMap()
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// Thread类中threadLocals的定义
public class Thread implements Runnable {
    // ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // ...
}

可以看到,getMap(Thread t)方法是从t对象中取出来了一个成员对象,其类型为ThreadLocal.ThreadLocalMap。t对象是一个Thread类,也就是说,真正的map是存储在Thread类中的一个成员变量,其名为threadLocals

下面,我们将重点转移到map.set(this, value)上:

// key为this对象,也就是当前的threadlocal对象
if (map != null)
    map.set(this, value);
else
    createMap(t, value);

key为this对象,由于当前set()方法是ThreadLocal中的一个成员方法,因此,调用到这个成员方法时,this指针是指向当前ThreadLocal对象。也就是例子中的tl对象。下面用一幅图来表示它们之间的关系:

ThreadLocal.png

这个地方key是一个虚线指向ThreadLocal对象,那是因为key是一个弱引用,后面会分析具体原因。

2.3 get()

// ThreadLocal类中的set()
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 从当前线程中获取map,注意map是定义在Thread类中的对象
    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();
}

set()方法几乎一致,先从当前线程获取map,然后传入当前ThreadLocal对象,获取对应的value。


三、内存泄露的隐患

  1. ThreadLocal对象的内存泄露
  2. value对象的内存泄露

3.1 ThreadLocal对象的内存泄露

ThreadLocal.png

由前面的分析,我们知道,一个ThreadLocal对象有两个指针指向它。一个是我们声明变量的时候,有一个强引用指针指向它。第二个是当我们为其设置值的时候,会在map中声明一个key,也是一个指向ThreadLocal对象的指针。若我们在代码中直接写tl = null,虽然我们后面用不到这个ThreadLocal对象,但它依然有一个key指针指向它,也就是说它无法被垃圾收集器回收,便产生了内存泄漏。下面我们来看一看在源码中是如何避免这个问题的:

// 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);
}

在ThreadLocal的set()中调用到了ThreadLocalMap中的set()方法:

// ThreadLocalMap类中的set()
private void set(ThreadLocal<?> key, Object value) {

    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)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // new了一个Entry,key为当前ThreadLocal对象
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

我们来看一下这个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;
    }
}

当我们将tl的值变为null的时候,内存中的ThreadLocal对象就只剩下一个指向它的弱引用指针了。我们又知道,被弱引用指针指向的对象,会在下一次垃圾回收的时候被回收掉,因此就避免了内存泄漏的问题。

3.2 value对象的内存泄露

ThreadLocal_value.png

value是一个前引用指针,指向一个object。若我们直接将ThreadLocalMap中的key赋值为null,也就是说我们在后面的代码中不需要使用value这个对象了。但value指向的object会一直存在,因此会发生内存泄漏。但ThreadLocal显然也考虑到了这个问题,为我们提供了remove()方法来解决它。

// ThreadLocal中的remove()
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        // 调用了ThreadLocalMap中的remove()方法
        m.remove(this);
}

// ThreadLocalMap中的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) {
            // 调用了Entry中的clear()方法,实现在其继承的Reference类中
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

// Reference类中的clear(0)
public void clear() {
    this.referent = null;
}

从以上的代码中可以看出,调用了remove()方法后,会将key对应的整个Entry的指针擦除,这样的话,在下一次垃圾回收到来之时,整个Entry对象都会被回收,也就避免了value对象的内存泄漏问题。


四、引用类型

这一部分是补充知识,如果已经清楚Java中的引用类型可以不用看这一部分了。

  1. 强引用:被强引用关联的对象不会被回收。使用new一个新对象的方式来创建强引用。

  2. 软引用:被软引用关联的对象只有在内存不足的情况下才会被回收。使用SoftReference类来创建软引用。

  3. 弱引用:被弱引用关联的对象一定会被回收,实际上它只能存活到下一次垃圾回收发生之前。使用WeakReference类来创建弱引用。

  4. 虚引用:又称为幽灵引用或幻影引用,一个对象是否有虚引用的存在,不会对生存时间造成影响,也无法通过虚引用得到一个对象。

    为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。

    使用PhantomReference来创建虚引用。

相关文章

网友评论

      本文标题:ThreadLocal

      本文链接:https://www.haomeiwen.com/subject/kiatahtx.html