什么是ThreadLocal?

简介: 这篇文章是慕课网上一门免费课程《ThreadLocal》的观后总结。这门课将ThreadLocal讲得非常清晰易懂,又深入底层原理和设计思想,是我看过的最好的ThreadLocal的资料,现在把用自己的话,把它整理成文字版本。总共预计产出四篇文章,这是第一篇。

这篇文章是慕课网上一门免费课程《ThreadLocal》的观后总结。这门课将ThreadLocal讲得非常清晰易懂,又深入底层原理和设计思想,是我看过的最好的ThreadLocal的资料,现在把用自己的话,把它整理成文字版本。

总共预计产出四篇文章,这是第一篇。


一致性问题


什么是一致性问题?

多线程充分利用了多核CPU的威力,为我们程序提供了很高的性能。但是有时候,我们需要多个线程互相协作,这里可能就会涉及到数据一致性的问题。

数据一致性指问题的是:发生在多个主体同一份数据无法达成共识。这里的多个主体,可能是多线程,也可能是多个服务器节点。

当然了,这里的“多个主体”也可以指朋友之间,夫妻之间,所谓“道不同,不相为谋”,说的就是这个理。

数据一致性问题是我们使用分布式或者多线程带来的代价,使得程序或系统变得复杂,那我们有什么办法可以解决它呢?

如何解决一致性问题?

在我们解决一致性问题的时候,大概有这样几种思路。

第一种是排队,如果两个人对一个问题的看法不一致,那就排成一队,一个人一个人去修改它,这样后面一个人总是能够得到前面一个人修改后的值,数据也就总是一致的了。我们在操作系统中的锁、互斥量、管程、屏障等等概念,都是利用了排队的思想。

排队虽然能够很好的确保数据一致性,但性能非常低。

第二种是投票,投票的话,多个人可以同时去做一件决策,或者同时去修改数据,但最终谁修改成功,是用投票来决定的。这个方式很高效,但它也会产生很多问题,比如网络中断、欺诈等等。想要通过投票达到一致性非常复杂,往往需要严格的数学理论来证明,还需要中间有一些“信使”不断来来回回传递消息,这中间也会有一些性能的开销。

我们在分布式系统中常见的Paxos和Raft算法,就是使用投票来解决一致性问题的。感兴趣的同学可以去我的个人网站阅读我之前写的关于这两个算法的文章。

分布式一致算法

第三种是避免。既然保证数据一致性很难,那我能不能通过一些手段,去避免多个线程之间产生一致性问题呢?我们程序员熟悉的git就是这个实现,大家在本地分布式修改同一个文件,最后通过版本控制和“冲突解决”去解决这个问题。

而我们今天的正题,ThreadLocal,也是使用的“避免”这种方式。

我们不能避免所有的数据不一致问题,所以我们还是需要学习排队和投票,去解决不同场景的数据不一致问题。


ThreadLocal是什么?


定义

首先我们上定义:ThreadLocal提供了线程局部变量,一个线程局部变量在多个线程中,分别有独立的值(副本)。

是不是有些看不懂?我刚开始了解ThreadLocal的时候,也有点不明白这句话是什么意思。最大的疑惑是:既然是每个线程独有的,那我干嘛不直接在调用线程的时候,在相应的方法里面声明和使用这个局部变量?

后来才明白,同一个线程可能会调用到很多不同的类和方法,你可能需要在不同的地方,用到这个变量。如果自己去实现这么一个功能,成本其实挺大的。

ThreadLocal是一个可以开箱即用、无额外开销、线程安全的工具类,可以完美解决这个问题。

ThreadLocal并不是Java语言独有的,在几乎所有提供多线程特征的语言里面,都会有ThreadLocal的实现。在Java中,ThreadLocal中用哈希表来实现的。

线程模型

这个图能够比较直观地解释ThreadLocal的线程模型。

ThreadLocal的线程模型

其实并不复杂。左边的黑色大圆圈代表一个进程。进程里有一个线程表,红色波浪线代表一个个线程。

对于每一个线程来说,都有自己的独占数据。这些独占数据是进程来分配的,对于Java来说,独占数据很多都是在Thread类里面分配的,而每一个线程里面都有一个ThreadLocalMap的对象,它本身是一个哈希表,里面会放一些线程的局部变量(红色长方形)。ThreadLocal的核心也是这个ThreadLocalMap。

相关源码:

// Thread类里的变量:
ThreadLocal.ThreadLocalMap threadLocals = null;
// ThreadLocalMap的定义:
static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    // ...
}


基本API


基本API主要分成四个部分,分别是:

  • 构造函数ThreadLocal()
  • 初始化initialValue()
  • 访问器get/set
  • 回收remove

构造函数是一个泛型的,传入的类型是你要使用的局部变量变量的类型。初始化initialValue()用于如果你没有调用set()方法的时候,调用get()方法返回的默认值。如果不重载初始化方法,会返回null

如果调用了set()方法,再调用get()方法,就不会调用initialValue()方法。

如果调用了set(),再调用remove(),再调用get(),是会调用initialValue()的。

JDK 8提供了静态方法withInitial来进行更好的初始化。

示例代码:

public class ThreadLocalDemo {
    public static final ThreadLocal<String> THREAD_LOCAL = ThreadLocal.withInitial(() -> {
        System.out.println("invoke initial value");
        return "default value";
    });
    public static void main(String[] args) throws InterruptedException {
        new Thread(() ->{
            THREAD_LOCAL.set("first thread");
            System.out.println(THREAD_LOCAL.get());
        }).start();
        new Thread(() ->{
            THREAD_LOCAL.set("second thread");
            System.out.println(THREAD_LOCAL.get());
        }).start();
        new Thread(() ->{
            THREAD_LOCAL.set("third thread");
            THREAD_LOCAL.remove();
            System.out.println(THREAD_LOCAL.get());
        }).start();
        new Thread(() ->{
            System.out.println(THREAD_LOCAL.get());
        }).start();
        SECONDS.sleep(1L);
    }
}
// 输出:
first thread
second thread
invoke initial value
default value
invoke initial value
default value


4种核心场景


在实际项目中,ThreadLocal一般用来做什么呢?这里总结四种核心的应用场景。

资源持有

比如我们有三个不同的类。在一次Web请求中,会在不同的地方,不同的时候,调用这三个类的实例。但用户是同一个,用户数据可以保存在一个线程里。

线程资源持有

这个时候,我们可以在程序1把用户数据放进ThreadLocalMap里,然后在程序2和程序3里面去用它。

这样做的优势在于:持有线程资源供线程的各个部分使用,全局获取,降低编程难度

线程一致

这里以JDBC为例。我们经常会用到事务,它是怎么实现的呢?

线程资源一致性

原来,我们每次对数据库操作,都会走JDBC getConnection,JDBC保证只要你是同一个线程过来的请求,不管是在哪个part,都返回的是同一个连接。这个就是使用ThreadLocal来做的。

当一个part过来的时候,JDBC会去看ThreadLocal里是不是已经有这个线程的连接了,如果有,就直接返回;如果没有,就从连接池请求分配一个连接,然后放进ThreadLocal里。

这样就可以保证一个事务的所有part都在一个连接里。TheadLocal可以帮助它维护这种一致性,降低编程难度

线程安全

假设我们一个线程的调用链路比较长。在中途中出现异常怎么做?我们可以在出错的时候,把错误信息放到ThreadLocal里面,然后在后续的链路去使用这个值。使用TheadLocal可以保证多个线程在处理这个场景的时候保证线程安全。

线程安全

并发计算

分布式计算

如果我们有一个大的任务,可以把它拆分成很多小任务,分别计算,然后最终把结果汇总起来。如果是分布式计算,可能是先存储在自己的节点里。而如果是单机下的多线程计算,可以把每个线程的计算结果放进ThreadLocal里面,最后取出来汇总。

那么问题来了,怎么取出ThreadLocal的所有线程的值?且看下篇文章分析。

目录
相关文章
|
存储 数据库 索引
客户端存储 —— IndexedDB 实现分页查询(下)
客户端存储 —— IndexedDB 实现分页查询
733 0
|
NoSQL Redis 数据安全/隐私保护
redis设置密码后如何实现主从复制
redis设置密码后如何实现主从复制
281 0
|
缓存 负载均衡 Java
Java“TimeoutException”解决
Java中的“TimeoutException”通常在操作超时未完成时抛出。解决方法包括:增加超时时间、优化代码逻辑减少执行时间、使用异步处理或线程池提高效率。
1450 11
|
10月前
|
存储 安全 数据安全/隐私保护
4S店、分公司远程访问总部DMS系统,贝锐花生壳提供高性价比方案
在汽车销售与服务行业,DMS(经销商管理系统)是日常运营的重要工具,涵盖销售、库存、售后等模块。传统远程访问方案如专线或VPN成本高且复杂,而贝锐花生壳内网穿透提供了一种高效、安全、低成本的替代方案。无需公网IP和复杂配置,只需三步即可实现DMS系统的远程访问,并支持加密传输和精细访问控制,确保数据安全。
288 16
|
存储 Linux 数据中心
网络工程师如何很快划分出子网?
【10月更文挑战第7天】
794 0
网络工程师如何很快划分出子网?
|
存储 运维 关系型数据库
探索 Apache Paimon 在阿里智能引擎的应用场景
本文整理自Apache Yarn && Flink Contributor,阿里巴巴智能引擎事业部技术专家王伟骏(鸿历)老师在 5月16日 Streaming Lakehouse Meetup · Online 上的分享。
26205 34
探索 Apache Paimon 在阿里智能引擎的应用场景
|
数据采集 JavaScript 前端开发
网页抓取进阶:如何提取复杂网页信息
在信息爆炸时代,从复杂网页中高效抓取数据对开发者和分析师至关重要。本文探讨如何利用 `webpage` 对象结合代理IP技术,轻松抓取如大众点评这类动态加载且具备反爬机制的网站数据。通过 Python 的 `requests`、`BeautifulSoup` 和 `Selenium`,结合代理IP,详细讲解了如何应对动态内容加载、反爬机制等问题,并提供了具体代码实现。通过这种方法,可以批量抓取商家信息,为数据分析提供支持。
1249 1
网页抓取进阶:如何提取复杂网页信息
|
DataWorks 关系型数据库 MySQL
DataWorks实时数据导入:如何实现源源不断的数据流?
【8月更文挑战第22天】在数据处理领域,高效实时传输至关重要。阿里云DataWorks提供全面的数据集成服务,支持多种数据导入方式,尤其实时导入功能因高效处理能力备受欢迎。通过创建数据源与数据集,并配置实时同步任务,可实现数据从MySQL等源到DataWorks数据仓库的快速准确流入。此流程不仅提升了数据处理效率,也确保了数据实时性和准确性,为企业决策提供强有力的支持。
278 1
|
存储 SQL 分布式计算
物联网数据库 IoTDB 解析
Apache IoTDB 是专为物联网时序数据打造的数据库,提供数据采集、存储、分析的功能。IoTDB 提供端云一体化的解决方案,在云端,提供高性能的数据读写以及丰富的查询能力,针对物联网场景定制高效的目录组织结构,并与 Apache Hadoop、Spark、Flink 等大数据系统无缝打通;在边缘端,提供轻量化的 TsFile 管理能力,端上的数据写到本地 TsFile,并提供一定的基础查询能力,同时支持将 TsFile 数据同步到云端。
6696 102