ThreadLocal的使用及源码阅读

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里直接拿,不从线程池拿。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值