类ThreadLocal的使用

目录

前言

正文

1.方法get() 与 null

2.类ThreadLocal 存取数据流程分析  

3.验证线程变量的隔离性 

4.解决 get() 返回 null 的问题。 

5.验证重写initalValue()方法的隔离性

6.使用remove()方法的必要性 


前言

        变量值的共享可以使用 public static 变量的形式,所有的线程都使用同一个 public static 变量,那如果想实现每一个线程都有自己的变量该如何解决呢?JDK提供的ThreadLocal就可以派上用场了。

        类ThreadLocal主要的作用就是将当数据放入当前线程对象的Map里,这个Map是Thread类的实例变量。类ThreadLocal自己不管理也不存储任何数据,它只是数据和Map之间的中介和桥梁,通过Threadlocal将数据放入Map中,执行流程如下:

                       数据 -> ThreadLocal -> currentThread() -> Map

        执行后每个线程中的 Map 就有存自己的数据,Map 中的 key 存储的是 ThreadLocal 对象,value 就是存储的值,说明ThreadLocal 和 值之间是一对一关系,一个 ThreadLoca 只能关联一个值。每个线程中的 Map 的值只对当前线程可见,其他线程不可以访问当前线程对象中 Map 的值。

        线程、Map、值之间的关系可以比喻为:

        人(线程)随身有兜子(Map)、兜子(Map)里面有东西(数据),这么实现,线程随身也有自己的数据了,随时可以访问自己的数据了。

        由于Map中的key不可以重复,所以一个 ThreadLocal 对象对应一个 value,内存结构如图所示:

类ThreadLocal的使用_第1张图片

正文

1.方法get() 与 null

如果从未在 Thread 中的 Map 存储 ThreadLocal 对象对应的值,则 get() 方法返回 null。

创建测试代码如下:

public class get_null {
    public static ThreadLocal threadLocal = new ThreadLocal();
    public static void main(String[] args) {
        if (threadLocal.get() == null){
            System.out.println("从未放过值");
            threadLocal.set("我的值");
        }
        System.out.println(threadLocal.get());
        System.out.println(threadLocal.get());
    }
}

程序运行结果如图:

类ThreadLocal的使用_第2张图片

        从运行结果来看,第一次第哦啊用 t1 对象的get()方法时返回的值是 null 。调用 set() 方法并赋值后,可以顺利取出值并打印到控制台上。类ThreadLocal解决的是变量在不同线程之间的隔离性,就是不同的线程拥有自己的值,不同线程中的值是可以通过 ThreadLocal 类进行保存的。

         main 线程的兜子 ThreadLocal.ThreadLocalMap threadLocals = null;对象是由JVM进行实例化。

2.类ThreadLocal 存取数据流程分析  

测试代码:

public class Test {
    public static void main(String[] args) {
        ThreadLocal local = new ThreadLocal();
        local.set("我是任意的值");
        System.out.println(local.get());
    }
}

JDK 源码角度分析:

1)执行set方法的代码时,代码如下(源码在ThreadLocal.java中): 

public void set(T value) {
        Thread t = Thread.currentThread(); //对象t就是 main线程
        ThreadLocalMap map = getMap(t);    //从 main 线程中获得 ThreadLocalMap 
        if (map != null) {                 //若 map 值不等于null,进行set操作
            map.set(this, value);
        } else {                          //若 map 值等于 null, 创建map并进行set操作
            createMap(t, value);
        }                                
    }

2)执行代码 ThreadLocalMap map = getMap(t); 中的  getMap(t) 的源代码如下(源码在ThreadLocal.java中):

ThreadLocal.ThreadLocalMap getMap(Thread t) {//参数 t 就是前面传入的 main线程
            return t.threadLocals;
            //返回 main 线程中的 threadLocals 变量对应的 ThreadLocalMap对象
        }

3)threadLocals 变量对应的 ThreadLocalMap对象(源码在Thread.java中): 

对象 threadLocals 数据类型就是 ThreadLocal.ThreaddLocalMap,变量threadLocals 是 Thread类中的实例变量。

4)取得 Thread 中的ThreadLocal.ThreadLocalMap后,根据 map 对象值是不是 null来决定是否对其执行 set 或 create and set 操作。 

5)createMap() 方法的功能是创建一个新的 ThreadLocalMap,并在这个新的 ThreadLocalMap 中存储数据, ThreadLocalMap 中的 key 就是当前的 ThreadLocal 对象,值就是传入的value,createMap() 方法的源代码如下(源码在ThreadLocal.java中):

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

6)看一下new ThreadLocalMap(this,firstValue) 构造方法的源代码,如图(源码在ThreadLocal.java中)ThreadLocalMap是ThreadLocal中的静态内部类:

类ThreadLocal的使用_第3张图片

在源代码中可以发现,ThreadLocal 对象与 firstValue 封装进 Entry 对象中,并放入了 table[] 数组中,最后看一下 table[] 数组的声明。

7) table[] 数组的源码如下(源码在ThreadLocal.java中):

变量 table 就是 Entry[] 数组类型。

经过上面的 7 个步骤,成功将 value 通过 ThreadLocal 放入当前线程 currentThread() 中的ThreadLocalMap 对象里面。

8)当执行System.out.println(local.get());代码时,ThreadLocal.get()源代码如下(源码在ThreadLocal.java中):

 public T get() {
        Thread t = Thread.currentThread();              //t就是main线程
        ThreadLocalMap map = getMap(t);                 //从main线程中获得Map
        if (map != null) {                              //进入此分支,因为map不是null
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {                            //进入此分支因为Entry对象不是null
                @SuppressWarnings("unchecked")
                T result = (T)e.value;                  //从Entry对象中获得value并返回
                return result;
            }
        }
        return setInitialValue(); //上面两个分支没有走,找不到返回null
    }

9)上面的  8 个步骤就是就是set 和 get 的执行流程,比较麻烦,为什么不能直接向Thread类中的ThreadLocalMap 对象存取数据呢?这是不能实现的,原因如图:

变量threadLocals默认是包级访问,所以不能从外部直接访问该变量,也没有对应的 get 和 set 方法,只有用同一个包中的类可以访问threadLocals变量,而ThreadLocal和Thread恰好在同一个包中。Java.lang包下,同在lang包下的ThreadLocal可以访问Thread中的ThreadLocalMap。

        由于在同一个lang包下,所以外部代码通过 ThreadLocal就可以访问Thread类中的”私密对象“ThreadLocalMap了。

3.验证线程变量的隔离性 

实现通过ThreadLocal在每个线程中存储自己的私有数据。

public class ThreadLoalTest {
    static class Tools{
        public static ThreadLocal t1 = new ThreadLocal();
    }
    static class MyThreadA extends Thread{
        @Override
        public void run() {
            try{
                for (int i = 0; i < 10; i++) {
                    Tools.t1.set("A "+(i+1));
                    System.out.println("A get "+Tools.t1.get());
                    int sleepValue = (int) (Math.random()*1000);
                    Thread.sleep(sleepValue);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    static class MyThreadB extends Thread{
        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    Tools.t1.set("B "+(i+1));
                    System.out.println("    B get "+Tools.t1.get());
                    int sleepValue = (int) (Math.random()*1000);
                    Thread.sleep(sleepValue);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThreadA a = new MyThreadA();
        MyThreadB b = new MyThreadB();
        a.start();
        b.start();
        for (int i = 0; i < 10; i++) {
            Tools.t1.set("main "+(i+1));
            System.out.println("        main get "+Tools.t1.get());
            int sleepValue = (int)(Math.random()*1000);
            Thread.sleep(sleepValue);
        }
    }
}

运行结果:

类ThreadLocal的使用_第4张图片

控制台输出的结果表明通过ThreadLocal向每一个线程存储自己的私有数据,虽然3个线程都向 t1 对象中通过的 set() 存放数据值 ,但每个人都只能取出自己的数据,不能取出别人的。

创建新的代码示例,再次实验;

public class s5 {
    static class Tools{
        public static ThreadLocal t1 = new ThreadLocal();
    }
    static class MyThreadA extends Thread{
        @Override
        public void run() {
            try{
                for (int i = 0; i < 10; i++) {
                    if (Tools.t1.get() == null){
                        Tools.t1.set("A "+(i+1));
                    }
                    System.out.println("A get "+ Tools.t1.get());
                    int sleepValue = (int) (Math.random()*1000);
                    Thread.sleep(sleepValue);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    static class MyThreadB extends Thread{
        @Override
        public void run() {
            try {
                for (int i = 1; i < 10; i++) {
                    if (Tools.t1.get()==null){
                        Tools.t1.set("B "+(i+1));
                    }
                    System.out.println("    B get "+ Tools.t1.get());
                    int sleepValue = (int) (Math.random()*1000);
                    Thread.sleep(sleepValue);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThreadA a = new MyThreadA();
        MyThreadB b = new MyThreadB();
        a.start();
        b.start();
        for (int i = 2; i < 10; i++) {
            if (Tools.t1.get() == null){
                Tools.t1.set("main "+(i+1));
            }
            System.out.println("        main get "+ Tools.t1.get());
            int sleepValue = (int)(Math.random()*1000);
            Thread.sleep(sleepValue);
        }
    }
}

运行结果如图:

类ThreadLocal的使用_第5张图片

        在for循环中使用了if语句来判断当前线程的 ThreadLocalMap 中是否有数据,如果有则不再重复set,所以线程 a 存取值 1 ,线程 b 存取值 2 ,线程 main 存取值3。

        在第一次调用ThreadLocal 类的 get() 方法时返回值是 null,怎么实现第一次调用 get() 不返回 null 呢?我们继续学习一下。

4.解决 get() 返回 null 的问题。 

创建新的类继承自ThreadLocal类。 

 static class ThreadLocalExt extends ThreadLocal{
        @Override
        protected Object initialValue() {
            return "我是默认值不再为null";
        }
    }

覆盖 initialValue() 方法具有初始值,因为ThreadLocal.java中的initialValue()方法默认返回值就是null,所以要在子类中重写。源代码如下:

类ThreadLocal的使用_第6张图片

运行代码如下:

private static ThreadLocalExt t1 = new ThreadLocalExt();
    public static void main(String[] args) {
        if (t1.get() == null){
            System.out.println("从未放过值");
            t1.set("我的值");
        }
        System.out.println(t1.get());
        System.out.println(t1.get());
    }

运行结果如图:

类ThreadLocal的使用_第7张图片

此案例仅能证明 main 线程有自己的值,那其他线程是否会有自己的初始值呢?

5.验证重写initalValue()方法的隔离性

创建新的实例:

public class ThreadLocal33 {
    static class ThreadLocalExt extends ThreadLocal{
        @Override
        protected Object initialValue() {
            return new Date().getTime();
        }
    }
    static class Tools{
        public static ThreadLocalExt t1 = new ThreadLocalExt();
    }
    static class ThreadA extends Thread{
        @Override
        public void run() {
            try{
                for (int i = 0; i < 10; i++) {
                    System.out.println("在ThreadA线程中取值="+Tools.t1.get());
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        try {
            for (int i = 0; i < 10; i++) {
                System.out.println("    在main线程中取值="+Tools.t1.get());
                Thread.sleep(100);
            }
            Thread.sleep(5000);
            ThreadA threadA = new ThreadA();
            threadA.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

运行结果:

类ThreadLocal的使用_第8张图片

子线程和父线程有各自的默认值。

6.使用remove()方法的必要性 

ThreadLocalMap中的静态内置类Entry是弱引用类型,源代码如图:

类ThreadLocal的使用_第9张图片

弱引用的特点是,只要垃圾回收器扫描时发现弱引用类型的对象,则不管内存是否足够,都会回收弱引用的对象。也就是只要执行 gc 操作,ThreadLocal对象就立即销毁,代表key的值ThreadLocal对象会随着gc操作而销毁,释放内存空间,但value值却不会随着gc操作而销毁 ,这会出现内存溢出。

public class ThreadLocal_Remove {
    static class MyThreadLocal extends ThreadLocal{
        private static AtomicInteger count = new AtomicInteger(0);
        @Override
        protected void finalize() throws Throwable {
            System.out.println("MyThreadLocal finalize()"+count.addAndGet(1));
        }
    }
    static class Userinfo{
        private static AtomicInteger count = new AtomicInteger(0);

        @Override
        protected void finalize() throws Throwable {
            System.out.println("------------Userinfo protect void finalize"+count);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 9000; i++) {
            MyThreadLocal threadLocal = new MyThreadLocal();
            Userinfo userinfo = new Userinfo();
            threadLocal.set(userinfo);
            //threadLocal.remove();
        }
        MyThreadLocal threadLocal = new MyThreadLocal();
        System.out.println("9000 end!");
        List list = new ArrayList();
        for (int i = 0; i < 900000000; i++) {
            String newString = new String(""+(i+1));
            Thread.yield();
            Thread.yield();
            Thread.yield();
            Thread.yield();
        }
        //打印threadLocal实现强引用
        System.out.println("zzzzzzzzzzzzz"+threadLocal);
    }
}

程序运行后,在jvisualvm.exe工具中可以查看到9000个MyThreadLocal类的对象全部被 gc 垃圾回收了,因为它们是弱引用类型,只有一个强引用的MyThreadLocal对象得以保留。 如图:

类ThreadLocal的使用_第10张图片

但是Userinfo的实例却没有被 gc 回收,整整9000个Userinfo对象都保存在内存中,如图:

如果Userinfo对象数量更多,则一定会出现内存溢出,所以 static class ThreadLocalMap类中不用的数据要使用ThreadLocal类的remove()方法进行清楚,实现Userinfo类对象的垃圾回收,释放内存。

运行代码如下:

public static void main(String[] args) {
        for (int i = 0; i < 9000; i++) {
            MyThreadLocal threadLocal = new MyThreadLocal();
            Userinfo userinfo = new Userinfo();
            threadLocal.set(userinfo);
            threadLocal.remove();
        }
        MyThreadLocal threadLocal = new MyThreadLocal();
        System.out.println("9000 end!");
        List list = new ArrayList();
        for (int i = 0; i < 900000000; i++) {
            String newString = new String(""+(i+1));
            Thread.yield();
            Thread.yield();
            Thread.yield();
            Thread.yield();
        }
        //打印threadLocal实现强引用
        //至少在内存中保留一个threadLocal对象
        System.out.println("zzzzzzzzzzzzz"+threadLocal);
    }

在代码中对ThreadLocal对象调用了 remove()方法,清楚不使用的对象,再释放内存资源。

程序运行后内存只有一个MyThreadLocal类的实例,并且9000个Userinfo类的实例也被回收了。

类ThreadLocal的使用_第11张图片

此实验的结论就是:当ThreadLocalMap中的数据不再使用时,要手动执行ThreadLocal类的remove()方法,清楚数据,释放内存空间,不然会出现内存溢出。 

你可能感兴趣的:(多线程,java,jvm,开发语言)