【面试】ThreadLocal的理解

237 阅读3分钟

谈谈对ThreadLocal的理解

ThreadLocal是Java中提供的线程本地存储机制,叫做线程变量,主要是做数据隔离,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

@Slf4j
public class Ctest {

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

    public static void main(String[] args) {
        // 线程1,设置变量值
        new Thread(()->{
            // 睡眠1S
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 往tl里面设置变量
            tl.set(new Person("zhangsan"));
            // 打印输出
            log.info(String.valueOf(tl.get()));
        }).start();

        // 线程2,不设置变量值,去读1中设置的值
        new Thread(()->{
            // 睡眠2s
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 获取tl
            log.info(String.valueOf(tl.get()));
        }).start();
    }

    static class Person {
        String name = "zhangsan";
        public Person(String name) {
            this.name = name;
        }
    }
}

打印输出:

21:28:54.292 [Thread-0] INFO com.example.stream.test.Ctest - com.example.stream.test.Ctest$Person@550c0234
21:28:55.284 [Thread-1] INFO com.example.stream.test.Ctest - null

同一个容器,线程1获取到了值,线程2获取为null,这是为什么呢?其实这就是ThreadLocal线程局部变量的真正含义,就是ThreadLocal会为每个线程创建副本变量,那么线程只能读取到自己内部的副本变量,因为线程2没有设置变量,所以没办法获取到,它是线程私有容器。

当我们往各自线程中设置变量值的时候,是否真的能够获取到呢?

@Slf4j
public class Ctest {

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

    public static void main(String[] args) {
        // 线程1,设置变量值
        new Thread(()->{
            // 睡眠1S
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 往tl里面设置变量
            tl.set(new Person("zhangsan"));
            // 打印输出
            log.info(String.valueOf(tl.get()));
        }).start();

        // 线程2,设置变量值
        new Thread(()->{
            // 睡眠2s
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 往tl里面设置变量
            tl.set(new Person("zhangsan"));
            // 打印输出
            log.info(String.valueOf(tl.get()));
        }).start();
    }

    static class Person {
        String name = "zhangsan";
        public Person(String name) {
            this.name = name;
        }
    }
}

打印输出:

21:42:45.001 [Thread-0] INFO com.example.stream.test.Ctest - com.example.stream.test.Ctest$Person@550c0234
21:42:45.997 [Thread-1] INFO com.example.stream.test.Ctest - com.example.stream.test.Ctest$Person@57580186

这个时候就获取到了。并且我们两个线程设置的值一样,获取到的各自的对象hascode值不一样

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

获取当前线程对象,获取当前线程ThreadLocalMap对象,判断是否存在,不存在则创建一个Entry对象,存储则更新当前ThreadLocal的值,ThreadLocalMap中,是一个Entry[]数组,每个entry是一个map对象,key是当前ThreadLocal,value是Object值。

ThreadLocal在线程池中会造成内存溢出,解决办法是,在用完ThreadLocal之后,手动调用remove()方法,清除Entry对象。

ThreadLocal使用场景

ThreadLocal使用场景: 用来解决数据库连接、cookie,Session管理,Spring事务等,还有就是我们的日期格式化工具SimpleDateFormat,使用ThreadLocal解决它线程不安全的问题,其实就是做数据隔离

Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

Spring的事务场景: 通过@Transactional开启自动支持事务处理,假设在这样的业务操作过程中,添加员工的同时将其设置为操作员,分配其角色,在使用方法调用的时候形成方法依次向下调用,也就是事务的传播机制,其中的事务处理,他的底层就有ThreadLocal的使用。。

@Override
@Transactional(rollbackFor = Exception.class)
public void add(Employee employee) {
    // 添加员工
    empMapper.add(employee);
    // 调用操作员添加接口
    userService.add(employee);
    // 调用角色添加接口
    roleService.add(employee);
}

这个时候添加员工的方法就会自动支持事务的处理,此时userService和roleService都是通过@Autowired注入的,添加操作员和添加角色也会自动的支持事务处理,开启事务的时候,在使用方法调用的时候形成方法依次向下调用,他的底层就有ThreadLocal的使用。