美文网首页
ThreadLocal的使用及源码阅读

ThreadLocal的使用及源码阅读

作者: silence_J | 来源:发表于2020-05-16 15:30 被阅读0次

    ThreadLocal的含义及使用

    ThreadLocal看字面意思,Thread 线程,Local 本地,它是什么意思?通过一个小程序来解释:

    public class ThreadLocalTest_01 {
    
        volatile static Person person = new Person();
    
        public static void main(String[] args) {
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 输出name时,lisi已经被线程t2改为zhangsan
                System.out.println(person.name);
            }, "t1").start();
    
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 在t1线程没有输出name前,把name改成lisi
                person.name = "lisi";
            }, "t2").start();
        }
    
    }
    
    class Person {
        String name = "zhangsan";
    }
    

    创建一个Person类,属性name初始化为zhangsan,创建person对象。
    起两个线程t1、t2。t1睡2s后打印person.name,t2睡1s后把person.name改成lisi。因为t1在打印前,name被先睡醒的t2改成了lisi,所以最后打印lisi。

    上面这个程序说明创建一个对象后,它在多个线程之间是共享的。

    那么如何才能做到这个person对象在每个线程中都自己独有的一份呢?

    用ThreadLocal就能做到,可以在ThreadLocal中设置值,这个值是各自线程独有的,别的线程访问不到。

    下面看小程序里怎么使用:

    public class ThreadLocalTest_02 {
    
        // volatile static Person person = new Person();
        static ThreadLocal<Person> personThreadLocal = new ThreadLocal<>();
    
        public static void main(String[] args) {
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 线程t2在自己的threadLocal里初始化person对象后,在该线程中get为null
                System.out.println(personThreadLocal.get());
            }, "t1").start();
    
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 在当前线程t2的threadLocal里添加一个person对象
                personThreadLocal.set(new Person());
            }, "t2").start();
        }
    
    }
    
    class Person {
        String name = "zhangsan";
    }
    

    这个程序的输出结果为null。
    线程t2在自己threadLocal里添加了一个person对象,但是线程t1的threadLocal是拿不到这个对象的。

    ThreadLocal源码阅读

    明明是同一个ThreadLocal对象,那为什么一个线程设置值了之后,其它线程就读取不到呢?

    这需要看它的源码实现,先来看一下它的set方法:

    // 参数类型是创建ThreadLocal对象时使用泛型规定的
    public void set(T value) {
        // 获取当前线程t
        Thread t = Thread.currentThread();
        // 返回线程t的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // key是当前线程的threadLocal,value是要设置的值
            map.set(this, value);
        } else {
            // 没有的话就new一个ThreadLocalMap把值设置进去
            createMap(t, value);
        }
    }
    

    里面用到了一个map容器ThreadLocalMap,它的key是threadLocal,value就是专属这个线程的值。

    这个map到底是什么东西,再看一下getMap的源码:

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

    getMap(t)方法返回了这个线程的threadLocals变量。

    点击t.threadLocals,跳到了Thread类:

    // Thread类部分源码
    public class Thread implements Runnable {
        // 省略其它...
        ThreadLocal.ThreadLocalMap threadLocals = null;
        // 省略其它...
    }
    

    可以看到每个线程都有自己的一个ThreadLocalMap。

    这时就明白了 map.set(this, value)就是设置当前线程的map(threadLocals),相当于:Thread.currentThread().map(ThreadLocal,value) (伪代码)

    来梳理一下,上一部分小程序中线程t2执行personThreadLocal.set(new Person())时,是把person对象set到了自己的map里,t1线程去访问的也是自己的属于t1线程的map,所以是读不到值的,因此使用ThreadLocal的时候,用set和get就完全的把他隔离开了,就是自己线程里面所特有的,其它的线程是没有的。

    为什么要用ThreadLocal?

    根据Spirng的声明式事务来解析,为什么要用ThreadLocal。

    声明式事务一般是要通过数据库的,但是我们知道Spring结合Mybatis,是可以把整个事务写在配置文件中的,而这个配置文件里的事务,实际上是管理了一系列的方法,方法1、方法2、方法3....,

    这些方法里面可能写了:比如第1个方法写了去配置文件里拿到数据库连接Connection,第2个、第3个都是一样去拿数据库连接。然后声明式事务可以把这几个方法合在一起,视为一个完整的事务。

    如果说在这些方法里,每一个方法拿的连接,不是同一个对象,事务是不可能成功的。Connection会放到一个连接池里边,如果第1个方法拿的是第1个Connection,第2个拿的是第2个,第3个拿的是第3个,不同的Connection是不能形成一个完整的事务的。

    那怎么保证是同一个Connection呢?

    把这Connection放到这个线程的本地对象ThreadLocal里面,以后再拿的时候,实际是从ThreadLocal里拿的,第1个方法拿的时候就把Connection放到ThreadLocal里面,后面的方法要拿的时候,从ThreadLocal里直接拿,不从线程池拿。

    参考:马士兵《多线程与高并发》

    相关文章

      网友评论

          本文标题:ThreadLocal的使用及源码阅读

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