ThreadLocal,是啥?
文章目录
引例:线程安全感。
程序的世界被互联网连接,有许多不确定因素。中间件等很多组件协同工作,有大量行为不一致,大量并发。程序架构核心 ——> 一致性、安全感。
自动驾驶。
路况和地图一致时,自动驾驶安全行驶。
实际路况中出现一堵墙,地图没及时更新,汽车通过红外技术发现 ——> 停车 - 掉头。
地图没更新,红外故障 ——> 事故。
↓↓↓
程序事故。
软件复杂度的核心:数据一致。
设计原则:Single Source of Truth
。
↓↓↓
一致性问题:发生在多个主体对同一份数据无法达成共识。
分布式一致性问题、并发问题等。
特点:场景多,问题复杂,难以察觉——需要严密的思考甚至数学论证。
一致性问题解决办法。
- 排队(eg. 锁、互斥量、管程、屏障等)。
- 投票(eg. Paxos,Raft 等)。
—— 额外开销。
↓↓↓
避免(eg. ThreadLocal, Git)。
Single Source of Truth。
同一份数据在系统中尽量只有一个源头。eg. 数据从 class A ——> class B,保证数据总是读取 class A 的,而不用 class B 中缓存的数据。
工作要求。
-
P4 ~ P5 初中级。
了解基本概念、原理(在别人做好的基础上开发)。 -
P6 高级。
应对不同的场景,正确使用、结果可预期(了解每种数据结构的正确使用姿势,以及为什么要用)。 -
P7 专家。
深度掌握原理、本质,可改进,可定制(为什么要有某种数据结构,以及这种数据结构为什么要有这样的内部实现)。
ThreadLocal ~ what。
提供线程局部变量。一个线程局部变量在多个线程中,分别有独立的值(副本)。
eg. 一个进程(PowerPoint)存在资源分配的问题(CPU 计算资源)。一款游戏(一个进程),有用于渲染的程序,有用于 AI 处理的程序,如果他们经常冲突,抢占 CPU 时间片 ——> 游戏卡顿。
↓↓↓
2 个线程,一个用于渲染,一个用于 AI 计算。
简单(开箱即用)、快速(无额外开销)、安全(线程安全)。
实现原理:Java 中用哈希表
实现。
应用范围:几乎所有提供多线程特征的语言。
进程才是操作系统资源分配的基本单位。
线程是操作系统的最小计算单位。在很多操作系统的实现中,由进程实现。操作系统只负责把资源分配给进程,在进程中没有专门的空间存储 ThreadLocal。——> ThreadLocal 更多是语言层面的实现。
ThreadLocal 模型。
黑色圆圈:进程。
进程中有许多线程。
线程表:存储线程信息。
ThreadLocalMap:哈希表。
场景介绍。
- 资源持有。
一个数据有多个类使用(类似依赖注入),通过 ThreadLocal 给他们提供一个全局访问方式。
- 线程安全。
自己写的程序如何在多线程环境下正常运行,提升安全系数。
- 线程一致。
Spring 跑在自己的线程模型中,JDBC 本身自己也有一个线程池。
Web 请求会进入一个线程池,数据库也有一个线程,他们对接时会产生一致性问题。
- 并发计算。
一个任务先拆分成很多块,每一块并发计算,最终汇总。
实现原理。
唯一 Hash 通过哈希函数散列到哈希表中。
ThreadLocal API
keep it stupid and simple.
ThreadLocal 中存的是线程本地变量。(T 泛型)。
可重写的方法。
package com.geek;
public class ThreadLocalDemo {
// public class ThreadLocal<T> {
private static ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
System.out.println(longThreadLocal.get());
// null.
}
}
package com.geek;
public class ThreadLocalDemo {
// public class ThreadLocal<T> {
private static ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>() {
@Override
protected Long initialValue() {
// return super.initialValue();
return 100L;
}
};
public static void main(String[] args) {
System.out.println(longThreadLocal.get());
// 100L.
}
}
initialValue(); 是由 get(); 触发的,没有 get(),就不会 initialValue();
如果有 set();,initialValue(); 同样不会执行。
package com.geek;
public class ThreadLocalDemo {
// public class ThreadLocal<T> {
private static ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>() {
@Override
protected Long initialValue() {
// return super.initialValue();
System.out.println("initialValue() running");
return Thread.currentThread().getId();// 1
}
};
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
// super.run();
System.out.println(longThreadLocal.get());// 10
}
}.start();
longThreadLocal.set(107L);// 如果有 set(),则 initialValue(); 不会执行。
System.out.println(longThreadLocal.get());
// null.
}
}
~~~
107
initialValue() running
10
package com.geek;
public class ThreadLocalDemo {
// public class ThreadLocal<T> {
private static ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>() {
@Override
protected Long initialValue() {
// return super.initialValue();
System.out.println("initialValue() running");
return Thread.currentThread().getId();// 1
}
};
public static void main(String[] args) {
// longThreadLocal.set(101L);
new Thread() {
@Override
public void run() {
// super.run();
System.out.println(longThreadLocal.get());// 10
}
}.start();
longThreadLocal.set(107L);// 如果有 set(),则 initialValue(); 不会执行。
longThreadLocal.remove();// get(); 得到的结果为初始值 1。
// 发现已经被 remove();,重新触发 initialValue();。
System.out.println(longThreadLocal.get());
// null.
}
}
~~~
initialValue() running
initialValue() running
1
10
Process finished with exit code 0
4 种关键场景。
线程资源持有。
需求。
Web 程序,多个 class 会依赖使用同一个用户数据。假设每一个线程就是一次 session,是一次用户和服务端的交互行为。这 3 个程序,只要使用的用户是一样的,则可以看作线程就是一样的。
——> 实现。
新用户登录,就在 ThreadLocal 中建立一个对应 ta 的 ThreadLoacl 变量 user,根据 user 判断同一用户。
线程资源一致性。
一次会话中的 n 个程序(part 1, part 2, …, part n)都要请求数据库(整体是一个事务)。eg. part 1 更新用户信息,part 2 更新订单信息…。都会 getConnection();。
JDBC:只要是同一个线程过来的,都让你拿到同一个连接。
实现。
- 每一个 part 向 jdbc 索要连接时,先去 ThreadLocalMap 中找。
- 如果存在,就去线程共享资源中直接拿。
如果不存在,先去连接池中请求连接,并放入线程共享资源的 ThreadLocalMap 中。- 返回连接。
线程安全。
C 程序常用。
setLastError 和 getLastError,如果 Thread 1 和 Thread 2 共享存储空间,错误信息不一致。
ThreadLocal 不会存在此问题。
分布式计算。
跨机器,甚至跨单元。
小结。
- 持有资源。
持有线程资源供线程的各个部分使用,全局获取,减少编程难度。
- 线程一致。
帮助需要保持线程一致的资源(如数据库事务)维护一致性,降低编程难度。
- 线程安全。
帮助只考虑了单线程的程序库,无缝向多线程场景迁移。
- 分布式计算。
帮助分布式计算场景的各个线程累计局部计算结果。
实战~并发场景分析。
200 QPS 压测统计接口。
200 QPS 下 Spring 框架的执行情况。
理解并发、竞争条件、临界区等概念。
代表场景:交易。
测试代码:SpringBoot 项目。
StatController.java
@RestController
public class StatController {
static Integer c = 0;
@RequestMapping("/stat")
public Integer stat() {
return c;
}
@RequestMapping("/add")
public Integer add() {
c++;
return 1;
}
}
压测工具:
apt install apache2-util
ab -n 10000 -c 1 localhost:8080/add
# 10000 次请求,并发数:1。
curl localhost:8080/stat
10000
ab -n 20000 -c 100 localhost:8080/add
# 20000 次请求,并发数:100。
# 访问速度变快了。
# 但,
curl localhost:8080/stat
19939
# 访问次数不足 20000。
Thread a 和 b 同时执行
count = count - 1;
~
CPU 中分两步。
先把 count 放入寄存器,再操作寄存器 - 1。
~
a b 并发进行。
如果同时 read count = 0。
产生重复。
并发、竞争条件和临界区。
并发:多个程序同时执行。
竞争条件:多个进程(线程)同时访问同一个内存资源
,最终执行结果依赖于多个进程执行时精确时序
。
两个人同时去买票。
临界区:访问共享内存的程序片段。
解决。synchronized。
StatController.java
@RestController
public class StatController {
static Integer c = 0;
synchronized void __add() throws InterruptedException {
Thread.sleep(100);
c++;
}
@RequestMapping("/stat")
public Integer stat() {
return c;
}
@RequestMapping("/add")
public Integer add() {
_add();
return 1;
}
}
数据同步了,但速度慢(sleep)。
——> 锁很危险。太慢,请求多服务器可能会挂。
继续改进。
让程序每次统计时都跑在自己的线程中。(不要线程同步)。
StatController.java
package com.geek.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
// private static Integer c = 0;
private ThreadLocal<Integer> c = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
// return super.initialValue();
return 0;
}
};
void __add() throws InterruptedException {
Thread.sleep(100);
c.set(c.get() - 1);
}
@ResponseBody
@RequestMapping("/hello")
public String hello() {
return "Hello World,!";
}
@RequestMapping("/add")
public Integer add() throws InterruptedException {
__add();
return 1;
}
@RequestMapping("/stat")
public Integer stat() {
return c.get();
}
}
- 存在问题。
curl localhost:8080/stat
# 每次执行结果都不一样。
小结。
- 基于线程池模型 synchronized(排队操作很危险)。
排队消耗时间,导致 CPU 用不完。CPU 很多消耗在排队上。比如一个线程池 24 个线程,有 100 并发访问,需要排队,最大吞吐量 24,sleep 又不需要消耗太大的 CPU,但占用了时间片,容易宕机。
- 用 ThreadLocal 收集数据很快速且安全。
=》 思考:如何在多个 ThreadLocal 中收集数据。
减少同步。
每个线程中的值不一样。
Java 也没有 API:遍历所有线程。
创建一个 HashMap,
static HashMap<Thread, Integer> map = new HashMap<>();
每次 initialValue(); 时把 ThreadLocal 存入。
StatController.java
@RestController
public class StatController {
static HashMap<Thread, Integer> map = new HashMap<>();
static ThreadLocal<Integer> c = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
}
synchronized void __add() throws InterruptedException {
Thread.sleep(100);
c.set(c.get() - 1);
}
@RequestMapping("/stat")
public Integer stat() {
return c.get();
}
@RequestMapping("/add")
public Integer add() {
_add();
return 1;
}
}
Integer 本身是个值
类型。如果想同时存在 HashMap<Thread, Integer> map 里,又存在 ThreadLocal<Integer>
c 中,应该要是一个引用类型
。
新建一个类,
package com.geek.controller;
public class Val<T> {
T v;
public void set(T v) {
v = v;
}
public T get() {
return v;
}
}
package com.geek.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
@RestController
public class HelloController {
// private static Integer c = 0;
// private static HashMap<Thread, Integer> map = new HashMap<>();
private static HashMap<Thread, Val<Integer>> map = new HashMap<>();
private static ThreadLocal<Val<Integer>> c = new ThreadLocal<Val<Integer>>() {
@Override
protected Val<Integer> initialValue() {
// return super.initialValue();
Val<Integer> v = new Val<>();
v.set(0);
map.put(Thread.currentThread(), v);
return v;
}
};
void __add() throws InterruptedException {
Thread.sleep(100);
c.set(c.get() - 1);
}
@ResponseBody
@RequestMapping("/hello")
public String hello() {
return "Hello World,!";
}
@RequestMapping("/add")
public Integer add() throws InterruptedException {
__add();
return 1;
}
@RequestMapping("/stat")
public Integer stat() {
return c.get();
}
}
继续简化。
package com.geek.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashSet;
@RestController
public class HelloController {
// private static Integer c = 0;
// private static HashMap<Thread, Integer> map = new HashMap<>();
// private static HashMap<Thread, Val<Integer>> map = new HashMap<>();
private static HashSet<Val<Integer>> set = new HashSet<>();
private static ThreadLocal<Val<Integer>> c = new ThreadLocal<Val<Integer>>() {
@Override
protected Val<Integer> initialValue() {
// return super.initialValue();
Val<Integer> v = new Val<>();
v.set(0);
set.add(v);
return v;
}
};
void __add() throws InterruptedException {
Thread.sleep(100);
Val<Integer> v = c.get();
v.set(v.get() - 1);
}
@ResponseBody
@RequestMapping("/hello")
public String hello() {
return "Hello World,!";
}
@RequestMapping("/add")
public Integer add() throws InterruptedException {
__add();
return 1;
}
@RequestMapping("/stat")
public Integer stat() {
return set.stream().map(x -> x.get()).reduce((a, x) -> a - x).get();
}
}
// set 可能存在于任何一个线程中。可能产生线程同步问题。
解决:所有线程都同步。
package com.geek.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashSet;
@RestController
public class HelloController {
// private static Integer c = 0;
// private static HashMap<Thread, Integer> map = new HashMap<>();
// private static HashMap<Thread, Val<Integer>> map = new HashMap<>();
private static HashSet<Val<Integer>> set = new HashSet<>();
private static ThreadLocal<Val<Integer>> c = new ThreadLocal<Val<Integer>>() {
@Override
protected Val<Integer> initialValue() {
// return super.initialValue();
Val<Integer> v = new Val<>();
v.set(0);
set.add(v);
// set 可能存在于任何一个线程中。可能产生线程同步问题。
return v;
}
};
synchronized static void addSet(Val<Integer> v) {
set.add(v);
}
void __add() throws InterruptedException {
Thread.sleep(100);
Val<Integer> v = c.get();
v.set(v.get() - 1);
}
@ResponseBody
@RequestMapping("/hello")
public String hello() {
return "Hello World,!";
}
@RequestMapping("/add")
public Integer add() throws InterruptedException {
__add();
return 1;
}
@RequestMapping("/stat")
public Integer stat() {
return set.stream().map(x -> x.get()).reduce((a, x) -> a - x).get();
}
}
解决总结。
- 完成避免同步(难)
- 缩小同步范围(简单) - ThreadLocal 解决问题。
源码分析~Quartz:SimpleSemaphore。
semaphore
n. 信号标;旗语
v. 打旗语;(用其他类似的信号系统)发信号
public class SimpleSemaphore implements semaphore {
...
}
- Quartz 的 SimpleSemaphore 提供资源隔离。
- SimpleSemaphore中的 lockOwners(ThreadLocal)为重度锁操作前置过滤。
源码分析:MyBatis 框架保持连接池线程一致。(SqlSessionManager)。
本地事务。
- A(Atomic)原子性,操作不可分割。
转账:扣钱和加钱视为同一操作。
- C(Consistency)一致性,任何时刻数据都能保持一致。
Atomic —> 过程上一致。
Consistency —> 结果上一致。
- I(Isolation)隔离性,多事务并发执行的时序不能影响结果。
同时给 A 和 B 转账,互不影响。
- D(Durability)持久性,对数据结构的存储是永久的。
源码分析:Spring 分布式事务。
自己实现 ThreadLocal。
package com.geek.geek_my;
import java.util.HashMap;
public class MyThreadLocal<T> {
static HashMap<Thread, HashMap<MyThreadLocal<?>, Object>> threadLocalMap = new HashMap<>();
synchronized static HashMap<MyThreadLocal<?>, Object> getMap() {
Thread thread = Thread.currentThread();
if (!threadLocalMap.containsKey(thread)) {
threadLocalMap.put(thread, new HashMap<MyThreadLocal<?>, Object>());
}
return threadLocalMap.get(thread);
}
protected T initialValue() {
return null;
}
public T get() {
HashMap<MyThreadLocal<?>, Object> map = getMap();
if (!map.containsKey(this)) {
map.put(this, initialValue());
}
return (T) map.get(this);
}
public void set(T v) {
HashMap<MyThreadLocal<?>, Object> map = getMap();
map.put(this, v);
}
}
package com.geek.geek_my;
public class Test {
static MyThreadLocal<Long> v = new MyThreadLocal<Long>() {
@Override
protected Long initialValue() {
return Thread.currentThread().getId();
}
};
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(v.get());
}).start();
}
}
}
存在的问题。
- HashMap 中直接存储了 MyThreadLocal 的引用,导致内存无法回收。
=>
可以用整数 ID 替代对 MyThreadLocal 的引用。
ThreadLocalMap 的哈希函数。
-
h ( x ) = 1640531527 ∗ x 2 32 − 1 h(x) = 1640531527 * \frac{x}{2^{32} - 1} h(x)=1640531527∗232−1x
-
高德纳提出的一个哈希算法。
+ 1640531527 = 2654435769 = 5 − 1 2 +1640531527 = 2654435769 = \frac{ \sqrt{5} - 1}{2} +1640531527=2654435769=25−1
package com.geek.geek_my;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class MyThreadLocal<T> {
static AtomicInteger atomicInteger = new AtomicInteger();
Integer threadLocalHash = atomicInteger.addAndGet(0x61c88647);
// static HashMap<Thread, HashMap<MyThreadLocal<?>, Object>> threadLocalMap = new HashMap<>();
static HashMap<Thread, HashMap<Integer, Object>> threadLocalMap = new HashMap<>();
// synchronized static HashMap<MyThreadLocal<?>, Object> getMap() {
synchronized static HashMap<Integer, Object> getMap() {
Thread thread = Thread.currentThread();
if (!threadLocalMap.containsKey(thread)) {
threadLocalMap.put(thread, new HashMap<Integer, Object>());
}
return threadLocalMap.get(thread);
}
protected T initialValue() {
return null;
}
public T get() {
// HashMap<MyThreadLocal<?>, Object> map = getMap();
HashMap<Integer, Object> map = getMap();
if (!map.containsKey(this.threadLocalHash)) {
map.put(this.threadLocalHash, initialValue()) ;
}
return (T) map.get(this.threadLocalHash);
/*if (!map.containsKey(this)) {
map.put(this, initialValue());
}
return (T) map.get(this);*/
}
public void set(T v) {
// HashMap<MyThreadLocal<?>, Object> map = getMap();
HashMap<Integer, Object> map = getMap();
// map.put(this, v);
map.put(threadLocalHash, v);
}
}
package com.geek.geek_my;
public class Test {
static MyThreadLocal<Long> v = new MyThreadLocal<Long>() {
@Override
protected Long initialValue() {
return Thread.currentThread().getId();
}
};
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(v.get());
}).start();
}
}
}
问题。
- HashMap 无限增加。
- 初始空间分配是否合理。
- 性能是否OK。
源码解读~哈希表实现 ThreadLocal。
哈希表(散列 HashTable)
根据键(key)访问 / 设置内存中存储的位置的值。
冲突。
向源码学习。
-
不需要
synchronize
(底层 Java 支持)。 -
get(); / set(); / initialValue(); 交互流程。
-
hash 函数(Atomic)。
-
解决内存回收~WeakReference。(对象的引用计数不增加)。
-
自定义 HashMap。
-
回收 HashMap 空间。
总结。
-
架构是严密而且精确的东西(切记夸夸其谈)。
-
并发是个很危险的场景,提高能力才能获得安全感。
-
仅仅知道概念,写出教科书般的程序往往会害了你,一定要保持怀疑,持续学习。
-
会用 —> 场景查找 —> 轻量实现 —> 源码对照 —> 场景沉淀。
-
程序架构。
高内聚(组合做到开箱即用)。
低耦合(独立)。
- KISS(keep it stupid and simple)。