多线程编程中的ThreadLocal详解

分类: Java

保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/67/ )。

防止一个对象被多个线程同时读写是多线程编程中非常重要的工作,通常可以使用加锁等方式来实现。那有没有一种方式来彻底避免这种情况呢?

有,ThreadLocal就是其中的一种。

一个对象会被多个线程访问,是因为多个线程共享了这个对象。我们只要把对象转变为线程独有的,就可以避免这种情况。这是一种以“空间换时间”的思路:

  • 当一个对象被多个线程共享时,节约了存储该对象的空间;但是在访问该对象时需要多个线程排队进行,这样便浪费了时间。
  • 当我们对象设置为线程独享时,每个线程都可以无需排队自由访问对象,节约了时间;但是同一个对象可能在多个线程中存在拷贝,这样就浪费了存储空间。

当然,有一些数据在多个线程之间共享是多个线程之间通讯的需要,这种情况不在此列。

时间与空间的矛盾在程序设计中会经常出现,我们需要根据不同的场景选择不同的方案。而ThreadLocal是典型的“时间换空间”的思路,每个线程都独有一个ThreadLocal,可以在其中存放独属于该线程的数据。

ThreadLocal的主要方法有:

  • T get(): 从ThreadLocal中读取数据
  • void set(T value):向ThreadLocal写入数据
  • void remove() :从ThreadLocal中删除数据

接下来我们通过示例展示ThreadLocal的使用,并证明ThreadLocal空间是归各个线程独享的。

在下面的代码中,我们创建threadLocalNumber和threadLocalString这两个ThreadLocal变量。

public class DemoApplication {
    // 创建了两个ThreadLocal变量
    private static ThreadLocal<Integer> threadLocalNumber = new ThreadLocal<>();
    private static ThreadLocal<String> threadLocalString = new ThreadLocal<>();

    public static void main(String[] args) {
        try {
            Thread thread01 = new Thread(new Task01());
            Thread thread02 = new Thread(new Task02());
            thread01.start();
            thread02.start();
            Thread.sleep(2L);
            System.out.println("Main-number: " + threadLocalNumber.get());
            System.out.println("Main-string: " + threadLocalString.get());
        } catch (Exception ex) {
            ex.printStackTrace();
        }

    }

    private static class Task01 implements Runnable {
        @Override
        public void run() {
            System.out.println("Thread01-number: " + threadLocalNumber.get());
            System.out.println("Set Thread01-number : 3001");
            threadLocalNumber.set(3001);
            System.out.println("Thread01-number: " + threadLocalNumber.get());
            System.out.println("Set Thread01-string : hello thread01");
            threadLocalString.set("hello thread01");
            System.out.println("Thread01-string: " + threadLocalString.get());
        }
    }

    private static class Task02 implements Runnable {
        @Override
        public void run() {
            System.out.println("Set Thread02-number : 3002");
            threadLocalNumber.set(3002);
            System.out.println("Thread02-number: " + threadLocalNumber.get());
            System.out.println("Thread02-string: " + threadLocalString.get());
            System.out.println("Set Thread02-string : hello thread02");
            threadLocalString.set("hello thread02");
            System.out.println("Thread02-string: " + threadLocalString.get());
        }
    }
}

上述代码中一种有三个线程,分别是main方法所在的主线程、执行Task01任务的thread01线程、执行Task02任务的thread02线程。在每个线程中,我们都对这两个ThreadLocal变量进行读写操作。

运行以上代码,我们可以看到下图所示的结果。

程序运行结果

因为是多线程并发执行,多个线程之间的打印顺序可能与上图存在差异。

通过打印出来的变量结果可知,每个线程操作的ThreadLocal变量都是线程内部的变量,不会对其他线程造成任何干扰。

在多线程程序中,当我们需要保存一些线程独有的数据时,可以借助ThreadLocal来实现。

以上内容均参考《通用源码阅读指导书——MyBatis源码详解》一书。

通用源码阅读指导书-京东自营

《通用源码阅读指导书》

这是一本以MyBatis的源码为实例讲述源码阅读方法的书籍,并且附带有示例项目源码,MyBatis的全中文注解。书籍还总结了大量的编程知识和架构经验,对提升编程和架构能力十分有用。

书中的“第23章 session包”章节介绍的MyBatis中Session的创建和存储时便使用ThreadLocal实现了多个线程中连接的隔离,感兴趣的读者可以自行阅读。

可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。

作者书籍推荐

作者书籍推荐 作者书籍推荐