什么是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的所有线程的值?且看下篇文章分析。

目录
相关文章
|
网络协议 Linux 网络安全
Centos配置静态ip地址 (保姆级教程!!!)(上)
Centos配置静态ip地址 (保姆级教程!!!)
1216 0
|
12月前
|
SQL 关系型数据库 MySQL
阿里云RDS云数据库全解析:产品功能、收费标准与活动参考
与云服务器ECS一样,关系型数据库RDS也是很多用户上云必买的热门云产品之一,阿里云的云数据库RDS主要包含RDS MySQL、RDS SQL Server、RDS PostgreSQL、RDS MariaDB等几个关系型数据库,并且提供了容灾、备份、恢复、监控、迁移等方面的全套解决方案,帮助您解决数据库运维的烦恼。本文为大家介绍阿里云的云数据库 RDS主要产品及计费方式、收费标准以及活动等相关情况,以供参考。
|
Java
Appium问题解决方案(8)- selenium.common.exceptions.WebDriverException: Message: An unknown server-side error occurred while processing the command. Original error: Could not sign with default certificate.
Appium问题解决方案(8)- selenium.common.exceptions.WebDriverException: Message: An unknown server-side error occurred while processing the command. Original error: Could not sign with default certificate.
2009 0
Appium问题解决方案(8)- selenium.common.exceptions.WebDriverException: Message: An unknown server-side error occurred while processing the command. Original error: Could not sign with default certificate.
|
10月前
|
存储 安全 Java
|
9月前
|
安全 算法 网络协议
深入理解 HTTPS RSA 握手:从原理到流程的完整解析
本文深入解析HTTPS中基于RSA的TLS握手过程,从加密、认证、完整性三大安全目标出发,详解四次握手流程、三个随机数作用及会话密钥生成机制,剖析数字证书验证与信任链原理,并指出RSA不支持前向保密的缺陷,揭示为何ECDHE成为主流。全面掌握HTTPS安全基石。
917 4
|
11月前
|
Linux 分布式数据库 数据库
OceanBase all-in-one 4.2.0.0 安装教程(CentOS 7/EL7 一键部署详细步骤)​
OceanBase all-in-one 4.2.0.0 是蚂蚁集团推出的单机集成版分布式数据库,解压即用,内置服务、工具与运行环境,支持 CentOS/RHEL 7 x86_64,适合学习、测试与开发。
1275 0
|
Java Apache Spring
Java发送Http请求(HttpClient)
Java发送Http请求(HttpClient)
13790 2
|
消息中间件 Java 大数据
Kafka ISR机制详解!
本文详细解析了Kafka的ISR(In-Sync Replicas)机制,阐述其工作原理及如何确保消息的高可靠性和高可用性。ISR动态维护与Leader同步的副本集,通过不同ACK确认机制(如acks=0、acks=1、acks=all),平衡可靠性和性能。此外,ISR机制支持故障转移,当Leader失效时,可从ISR中选取新的Leader。文章还包括实例分析,展示了ISR在不同场景下的变化,并讨论了其优缺点,帮助读者更好地理解和应用ISR机制。
2157 0
Kafka ISR机制详解!
|
前端开发 JavaScript 数据库
在 React 项目中 Editable Table 的实现
由于我们是把 state 存放在父组件的,每次请求会造成 table 进行 render 一遍,如果再加入 loading 等状态,render 次数会更多。Table 组件默认情况下没有对 rerender 行为做优化,父组件更新,如果 columns 中的提供了自定义 render 方法, 对应的每个 Cell 都会重新 render 。 针对这种情况我们就需要进行优化,根据 shouldCellUpdate 来自定义渲染时机。 那么每个 Cell 的渲染时机应该是: 1. FormItem 增删位置变动时 2. 该 Cell 消费的对应 tableOptions 变动时 第一种情况很好判
652 1
|
开发工具 Android开发
[Android Studio]please select android sdk
[Android Studio]please select android sdk
1246 1