查看原文
其他

【并发技术07】使用ThreadLocal在线程范围内共享数据

倪升武 武哥聊编程 2022-08-24


在上一篇文章中我们总结了一下,线程范围内的数据共享问题,即定义一个 Map,将当前线程名称和线程中的数据以键值对的形式存到 Map 中,然后在当前线程中使用数据的时候就可以根据当前线程名称从 Map 中拿到当前线程中的数据,这样就可以做到不同线程之间数据互不干扰。其实 ThreadLocal 类就是给我们提供了这个解决方法,所以我们完全可以用 ThreadLocal 来完成线程范围内数据的共享。

  1. public class ThreadScopeShareData {

  2.    //定义一个ThreadLocal

  3.    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();

  4.    public static void main(String[] args) {

  5.        for(int i = 0; i < 2; i ++) {

  6.            new Thread(new Runnable() {

  7.                @Override

  8.                public void run() {

  9.                    int data = new Random().nextInt();

  10.                    System.out.println(Thread.currentThread().getName() + " has put a data: " + data);

  11.                    threadLocal.set(data);//直接往threadLocal里面里面扔数据即可

  12.                    new TestA().getData();

  13.                    new TestB().getData();

  14.                }

  15.            }).start();

  16.        }

  17.    }

  18.    static class TestA {

  19.        public void getData() {

  20.            System.out.println("A get data from " + Thread.currentThread().getName() + ": " + threadLocal.get());//直接取,不用什么关键字,它直接从当前线程中取

  21.        }

  22.    }

  23.    static class TestB {

  24.        public void getData() {

  25.            System.out.println("B get data from " + Thread.currentThread().getName() + ": " + threadLocal.get());//直接取,不用什么关键字,它直接从当前线程中取

  26.        }

  27.    }

  28. }

结合上一节的代码,可以看出,其实 ThreadLocal 就相当于一个 Map,只不过我们不需要设定 key 了,它默认就是当前的 Thread,往里面放数据,直接 set 即可,取数据,直接 get 即可,很方便,就不用 Map 一个个存了,但是问题来了, ThreadLocal 虽然存取方便,但是 get() 方法中根本没有参数,也就是说我们只能往 ThreadLocal 中放一个数据,多了就不行了,那么该如何解决这个问题呢?

很明显, ThreadLocal 是个容器,且只能存一下,那么如果有多个数据,我们可以定义一个类,把数据都封装到这个类中,然后扔到 ThreadLocal 中,用的时候取这个类,再从类中去我们想要的数据即可。

好,现在有两个线程,每个线程都要操作各自的数据,而且数据有两个:名字和年龄。根据上面的思路,写一个 demo,如下:

  1. public class ThreadScopeShareData {

  2.    private static ThreadLocal<User> threadLocal = new ThreadLocal<User>();

  3.    public static void main(String[] args) {

  4.        for(int i = 0; i < 2; i ++) {//开启两个线程

  5.            new Thread(new Runnable() {

  6.                @Override

  7.                public void run() {

  8.                    int data = new Random().nextInt();

  9.                    System.out.println(Thread.currentThread().getName() + " has put a data: " + data);

  10.                    //每个线程中维护一个User,User中保存了name和age

  11.                    User user = new User();

  12.                    user.setName("name" + data);

  13.                    user.setAge(data);

  14.                    threadLocal.set(user); //向当前线程中存入user对象

  15.                    new TestA().getData();

  16.                    new TestB().getData();

  17.                }

  18.            }).start();

  19.        }

  20.    }

  21.    static class TestA {

  22.        public void getData() {

  23.            User user = threadLocal.get();//从当前线程中取出user对象

  24.            System.out.println("A get data from " + Thread.currentThread().getName() + ": "

  25.                    + user.getName() + "," + user.getAge());

  26.        }

  27.    }

  28.    static class TestB {

  29.        public void getData() {

  30.            User user = threadLocal.get();//从当前线程中取出user对象

  31.            System.out.println("B get data from " + Thread.currentThread().getName() + ": "

  32.                    + user.getName() + "," + user.getAge());

  33.        }

  34.    }

  35. }

  36. //定义一个User类来存储姓名和年龄

  37. class User {

  38.    private String name;

  39.    private int age;

  40.    public String getName() {

  41.        return name;

  42.    }

  43.    public void setName(String name) {

  44.        this.name = name;

  45.    }

  46.    public int getAge() {

  47.        return age;

  48.    }

  49.    public void setAge(int age) {

  50.        this.age = age;

  51.    }  

  52. }

这样进行一下封装就可以实现多个数据的存储了,但是上面这个程序是不太好的,原因很明显,在线程中,我要自己 new 一个对象,然后对其进行操作,最后还得把这个对象扔到当前线程中。这不太符合设计的思路,设计的思路应该是这样的,不能让用户自己去 new 啊,如果有个类似于 getThreadInstance() 的方法,用户想要从 ThreadLocal 中拿什么对象就用该对象去调用这个 getThreadInstance() 方法多好,这样拿到的永远都是本线程范围内的对象了。

这让我想到了学习 JDBC 的时候,从 ThreadLocal 中拿 connection 时的做法了,如果当前 ThreadLocal 中有就拿出来,没有就产生一个,这跟这里的需求是一样的,我想要一个 User,那我应该用 User 去调用 getThreadLInstance() 方法获取本线程中的一个 User 对象,如果有就拿,如果没有就产生一个。完全一样的思路。这个设计跟单例的模式有点像,这里说有点像不是本质上像,是代码结构很像。先看一下简单的单例模式代码结构:

  1. public class Singleton {

  2.    private static Singleton instance = null;

  3.    private Singleton() {//私有构造方法阻止外界new        

  4.    }

  5.    public static synchronized Singleton getInstance() {  //提供一个公共方法返回给外界一个单例的实例

  6.        if (instance == null) {  //如果没有实例

  7.            instance = new Singleton();  //就新new一个

  8.        }  

  9.        return instance;  //返回该实例

  10.    }

  11. }

这是懒汉式单例模式的代码结构,我门完全可以效仿该思路去设计一个从当前线程中拿 User 的办法,所以将程序修改如下:

  1. public class ThreadScopeShareData {

  2. //不需要在外面定义threadLocal了,放到User类中了

  3. //    private static ThreadLocal<User> threadLocal = new ThreadLocal<User>();

  4.    public static void main(String[] args) {

  5.        for(int i = 0; i < 2; i ++) {

  6.            new Thread(new Runnable() {            

  7.                @Override

  8.                public void run() {

  9.                    int data = new Random().nextInt();

  10.                    System.out.println(Thread.currentThread().getName() + " has put a data: " + data);

  11.                    //这里直接用User去调用getThreadLocal这个静态方法获取本线程范围内的一个User对象

  12.                    //这里就优雅多了,我完全不用关心如何去拿该线程中的对象,如何把对象放到threadLocal中

  13.                    //我只要拿就行,而且拿出来的肯定就是当前线程中的对象,原因看下面User类中的设计

  14.                    User.getThreadInstance().setName("name" + data);

  15.                    User.getThreadInstance().setAge(data);

  16.                    new TestA().getData();

  17.                    new TestB().getData();

  18.                }

  19.            }).start();

  20.        }

  21.    }

  22.    static class TestA {

  23.        public void getData() {

  24.            //还是调用这个静态方法拿,因为刚刚已经拿过一次了,threadLocal中已经有了

  25.            User user = User.getThreadInstance();

  26.            System.out.println("A get data from " + Thread.currentThread().getName() + ": "

  27.                    + user.getName() + "," + user.getAge());

  28.        }

  29.    }

  30.    static class TestB {

  31.        public void getData() {        

  32.            User user = User.getThreadInstance();

  33.            System.out.println("A get data from " + Thread.currentThread().getName() + ": "

  34.                    + user.getName() + "," + user.getAge());

  35.        }

  36.    }

  37. }

  38. class User {    

  39.    private User() {}

  40.    private static ThreadLocal<User> threadLocal = new ThreadLocal<User>();

  41.    //注意,这不是单例,每个线程都可以new,所以不用synchronized,

  42.    //但是每个threadLocal中是单例的,因为有了的话就不会再new了

  43.    public static /*synchronized*/ User getThreadInstance() {

  44.        User instance = threadLocal.get(); //先从当前threadLocal中拿

  45.        if(instance == null) {

  46.            instance = new User();

  47.            threadLocal.set(instance);//如果没有就新new一个放到threadLocal中

  48.        }

  49.        return instance; //向外返回该User

  50.    }

  51.    private String name;

  52.    private int age;

  53.    public String getName() {

  54.        return name;

  55.    }

  56.    public void setName(String name) {

  57.        this.name = name;

  58.    }

  59.    public int getAge() {

  60.        return age;

  61.    }

  62.    public void setAge(int age) {

  63.        this.age = age;

  64.    }

  65. }

经过这样的改造,代码就优雅多了,外界从来不要考虑如何去当前线程中拿数据,只要拿就行,拿出来的肯定就是当前线程中你想要的对象,因为在对象内部已经写好了这个静态方法了,而且拿出来 操作完了后,也不需要再放到 threadLocal 中,因为它本来就在 threadLocal 中,这就封装的相当好了。ThreadLocal 类的应用和使用技巧就总结这么多吧~

如果觉得对您有帮助,请转发给更多人吧~

关注“程序员私房菜”,学习更多技术干货,领取更多免费资源

↓↓↓


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存