ThreadLocal的基本使用、原理及可能存在的问题

ThreadLocal的使用
实现解析
引发的内存泄漏分析
ThreadLocal的线程不安全

ThreadLocal的使用

public class UseThreadLocal{

    private static ThreadLocal threadLocal = new ThreadLocal(){
        public Integer initialValue(){
            return 1;
        }
    };

    private static class TestThread implements Runnable{
        private int id;
        public TestThread(int id){
            this.id = id;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "====" + threadLocal.get()+" begin");
            Integer value = threadLocal.get();
            id = value + id;
            threadLocal.set(id);
            System.out.println(Thread.currentThread().getName() + "====" + threadLocal.get() +" end");
        }

    }

    public static void main(String[] args) {
        new Thread(new TestThread(1)).start();
        new Thread(new TestThread(1)).start();
    }

}

输出结果:

Thread-0====1 begin
Thread-0====2 end
Thread-1====1 begin
Thread-1====2 end

实现解析:

每个线程都有变量的副本,线程的隔离

每个Thread的对象都有一个ThreadLocalMap,当创建一个ThreadLocal的时候,就会将该ThreadLocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类型。

在该类中,我觉得最重要的方法就是两个:set()和get()方法。当调用ThreadLocal的get()方法的时候,会先找到当前线程的ThreadLocalMap,然后再找到对应的值。set()方法也是一样。


image.png

image.png

引发的内存泄漏分析:

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

但是这些被动的预防措施并不能保证不会内存泄漏:

使用线程池的时候,这个线程执行任务结束,ThreadLocal对象被回收了,线程放回线程池中不销毁,这个线程一直不被使用,导致内存泄漏。
分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么这个期间就会发生内存泄漏。
使用ThreadLocal最好是每次使用完就调用remove方法,将其删掉

ThreadLocal的线程不安全

下面我们举一个例子进行说明,Number是拥有一个int型成员变量的类:

public class Number {
    
    private int num;
 
    public int getNum() {
        return num;
    }
 
    public void setNum(int num) {
        this.num = num;
    }
 
    @Override
    public String toString() {
        return "Number [num=" + num + "]";
    }
    
}
 

  NotSafeThread是一个实现了Runable接口的类,其中我们创建了一个ThreadLocal类型的变量value,用来存放不同线程的num值,接着我们用线程池的方式启动了5个线程,我们希望使用ThreadLocal类为5个不同的线程都存放一个Number类型的副本,根除对变量的共享,并且在调用ThreadLocal类的get()方法时,返回与线程关联的Number对象,而这些Number对象我们希望它们都能跟踪自己的计数值:

public class NotSafeThread implements Runnable {
 
    public static Number number = new Number();
 
    public static int i = 0;
 
    public void run() {
        //每个线程计数加一
        number.setNum(i++);
     //将其存储到ThreadLocal中
        value.set(number);
        //输出num值
        System.out.println(value.get().getNum());
    }
 
    public static ThreadLocal value = new ThreadLocal() {
    };
 
    public static void main(String[] args) {
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            newCachedThreadPool.execute(new NotSafeThread());
        }
    }
 
}
public class NotSafeThread implements Runnable {
 
    public static Number number = new Number();
 
    public static int i = 0;
 
    public void run() {
        //每个线程计数加一
        number.setNum(i++);
        //将其存储到ThreadLocal中
        value.set(number);
        //延时2秒
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
        }
        //输出num值
        System.out.println(value.get().getNum());
    }
 
    public static ThreadLocal value = new ThreadLocal() {
    };
 
    public static void main(String[] args) {
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            newCachedThreadPool.execute(new NotSafeThread());
        }
    }
 
}
 

运行程序,输出:

4
4
4
4
4

原因是ThreadLocalMap中保存的是对象的引用,现在Number是共享的,都能修改
所以建议创建ThreadLocal的时候直接初始化

你可能感兴趣的:(ThreadLocal的基本使用、原理及可能存在的问题)