跳转至

ThreadLocal

线程数据共享和安全

1.什么是ThreadLocal?

  1. ThreadLocal的作用,可以实现在同一个线程数据共享,从而解决多线程数据安全问题

当http请求发送到Tomcat服务端时,Tomcat会创建一个线程去处理这个http请求,如果是请求servlet,servlet可能又会调用其他service,在这些service中,又可能会调用dao,去对数据库进行操作。

在这些资源或者方法的调用中,为解决数据安全问题,在这一个线程执行的过程中,我们希望有一个数据是共享的,而且是安全的。

应用场景:比如说事务控制,一个线程可能涉及到多个service的调用,调用多个dao,在这过程中,可能对数据库的多张表进行了操作。这时我们希望在整个业务流程结束之后,再进行一次提交commit。反过来说,在没有进行提交之前,我们希望始终是一个connection在操作,这样才能在结束时进行统一的一次提交(在开始操作的时候将自动提交设置为false)。

这样就可以解决同一个请求中,调用多个service或者多个dao的需求,这个需求也是开发中必须解决的事务安全问题(事务一致性需求)。

ThreadLocal技术就能够很好地解决这个问题。我们可以在实际开发中使用Filter和ThreadLocal解决事务安全问题。

  1. 一个ThreadLocal对象可以给当前线程关联**一个**数据(普通变量,对象,对组)--使用set方法

  2. ThreadLocal可以像Map一样存取数据,key为当前的ThreadLocal对象--使用get方法

  3. **每一个**ThreadLocal对象只能为当前线程关联**一个**数据,如果要为当前线程关联多个数据,就需要使用多个ThreadLocal对象实例

  4. 每个ThreadLocal对象实例定义的时候,一般为static类型

  5. ThreadLocal中保存的数据,在线程销毁之后,会自动释放

2.ThreadLocal快速入门

2.1ThreadLocal的类图

如下:ThreadLocal类中常用的方法有get(),set(),getMap()等,ThreadLocal类中含有一个重要的内部类ThreadLocalMap,ThreadLocalMap类中又含有一个内部类Entry,数据以key-value的形式存放在Entry中。

  • ThreadLocal核心的价值就是:在一个线程中,以线程安全的方式来共享数据。

2.2应用实例

需求: 演示 ThreadLocal (作用:在一个线程中, 共享数据(线程安全))的使用

image-20221209200900700

Dog:

package com.li.threadlocal;

public class Dog {
}

T1:

package com.li.threadlocal;

public class T1 {
    //创建ThreadLocal对象, public static修饰
    public static ThreadLocal<Object> threadLocal1 = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(new Task()).start();//启动一个新的线程,注意不是主线程
    }

    //Task是一个线程类,同时是一个内部类
    public static class Task implements Runnable {
        @Override
        public void run() {
            Dog dog = new Dog();
            Pig pig = new Pig();
            //给threadLocal1对象放入dog
            System.out.println("Task 放入了 dog=" + dog);
            threadLocal1.set(dog);
            System.out.println("Task 的 run 方法中的线程= " + Thread.currentThread().getName());
            new T1Service().update();
        }
    }
}

T1Service:

package com.li.threadlocal;

public class T1Service {
    public void update() {
        //取出threadLocal1对象关联的对象
        Object o = T1.threadLocal1.get();
        //获取当前线程名
        String name = Thread.currentThread().getName();
        System.out.println("在T1Service 的update()的线程是= " + name + ", 取出dog= " + o);
        //调用了dao-update()方法
        new T1DAO().update();
    }
}

T1DAO:

package com.li.threadlocal;

public class T1DAO {
    public void update() {
        //取出线程关联的threadLocal1对象的数据
        Object o = T1.threadLocal1.get();
        //获取当前线程的名称
        String name = Thread.currentThread().getName();
        System.out.println("在T1DAO 的update()的线程是= " + name + ", 取出dog= " + o);
    }
}

可以看到所有方法中拿到的对象都是同一个:

image-20221209203301559

3.源码分析

3.1ThreadLocal的set()

在2.2的应用实例中,我们在T1类中使用了ThreadLocal.set()方法,现在来看看set()方法的底层源码:

image-20221210170113118

set()方法关联的其他方法和属性:

image-20221210173822578

image-20221210173705583

image-20221210173317916

从上述代码中我们可以知道set方法的主要工作如下:

public void set(T value) {
     //1.获取当前线程
     Thread t = Thread.currentThread();

     //2.通过线程对象,获取到和此线程关联的ThreadLocalMap
     //   (ThreadLocalMap是ThreadLocal里的一个静态内部类*,
     //   类型是ThreadLocal$ThreadLocalMap)
     ThreadLocalMap map = getMap(t);

     //3.如果获取到的ThreadLocalMap不为空,就将传入的数据放入map,其中:
     //   -key为当前的ThreadLocal对象(this) -value为存放的数据,
     //   因为key值不能重复(map性质),一个ThreadLocal对象只能存放一个数据
     //   如果再赋值,就会替换旧的value值
     if (map != null)
         map.set(this, value);

      //4.如果和当前线程关联的ThreadLocalMap为null,
      //就创建一个和当前线程关联的ThreadLocalMap,并且将存放的数据作为value放入map,
      //这里的key为当前线程t(作用是让线程和创建的map关联起来)
     else       
         createMap(t, value);
}

在2.2应用实例的threadLocal1.set(dog);语句旁打上断点,点击debug,点击step over。

如下图所示,可以看到子线程Thread-0中有一个threadLocals属性(该属性的类型为ThreadLocalMap),该map中又有一个table属性(table的类型为Entry[]数组), table数组存放Entry对。

image-20221210175653999

这里涉及到的弱引用暂不深入

image-20221210171736237

在Entry对中,以k-v的形式存放数据,key值为 当前的线程中 的ThreadLocal对象,value值为存放的数据。

因为map的存放性质,如果在同一个ThreadLocal对象中存放多个value,那么在底层的Entry对中保存的是最近存放的value值,这也是为什么一个ThreadLocal对象只能存放一个值

线程中所有的ThreadLocal对象都被当前线程的threadLocals属性(map)管理。因此无论在哪个方法中,只要能找到对应的线程Thread,就对该线程关联的所有ThreadLocal对象中的value值进行操作。

一个线程中可以有多个ThreadLocal对象,如果还有其他ThreadLocal对象,使用set方法,存放的就是其他Entry对(key值就是其他的ThreadLocal对象)

存放多个ThreadLocal对象:

image-20221210182215032

3.2ThreadLocal的get()

image-20221210182538665

public T get() {
    //先得到当前的线程对象
    Thread t = Thread.currentThread();
    //获取和当前线程对象关联的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    //如果map不为空
    if (map != null) {
        //就根据当前的调用者this(即当前调用get方法的ThreadLocal对象),得到对应的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        //如果Entry的值e不为空
        if (e != null) {
            @SuppressWarnings("unchecked")
            //返回当前ThreadLocal对象关联的value值
            T result = (T)e.value;
            return result;
        }
    }
    //如果map为空,就初始化map,并将map和当前线程关联
    return setInitialValue();
}

3.3总结

image-20221210185538516