ThreadLocal,是啥?

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();

ThreadLocal 中存的是线程本地变量。(T 泛型)。

可重写的方法。

在这里插入图片描述



  • 初始化。initialValue();


  • 访问器。get / set。
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


  • 回收。remove。
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)=16405315272321x

  • 高德纳提出的一个哈希算法。

+ 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)。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lyfGeek

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值