tulip notes
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Star-Lord

希望一天成为大师的学徒
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java基础与面向对象

  • 高级进阶

  • 并发合集

    • Java中创建线程的几种方式
    • 并发相关概念与体系图
    • 线程状态与操作系统的用户态、内核态
    • 线程中的声明与守护线程_基础
    • 程序中的幽灵错误_基础
    • JDK并发包
    • 线程池相关
    • 并发中的安全集合
    • 生产者和消费者
    • 玩转单例模式
    • 一些工具类的原理
    • 并发包中的AQS
    • ThreadLocal与JMM
      • ThreadLocal的作用
        • 核心使用场景
        • 线程安全的上下文传递
        • 性能优化
        • ThreadLocal 的内存泄漏问题与解决方案
        • 一个例子-示例代码
        • 场景:模拟用户登录信息存储
        • 代码解析
      • JMM的三大特性
        • 三大特性的诞生
        • JMM的诞生
        • 原子性(Atomicity)
        • 保证原子性
        • 可见性(Visibility)
        • 保证可见性
        • 有序性(Ordering)
        • 保证有序性
        • 加锁过程是如何保证3个特性的
        • volatile的实现
        • synchronized的实现
    • 锁的探究
  • JVM合集

  • 实战与细节

  • 代码之丑与提升

  • 《Java》学习笔记
  • 并发合集
EffectTang
2025-02-18
目录

ThreadLocal与JMM

# ThreadLocal与JMM

ThreadLocal 是 Java 中用于实现线程封闭(Thread Confinement)的核心工具,它通过为每个线程提供独立的变量副本,避免多线程共享变量导致的并发问题。

# ThreadLocal的作用

ThreadLocal的作用是为每个线程提供独立的变量副本,避免多线程间的共享问题。常见的应用场景可能有用户会话信息、数据库连接管理、防止参数传递等。比如在Web应用中,每个请求可能对应一个线程,用ThreadLocal保存用户信息,方便在方法间传递,而不用显式传参。

# 核心使用场景

# 线程安全的上下文传递

场景:在多层方法调用中传递上下文信息(如用户身份、事务ID),避免显式传参。

示例:

  • Web 框架中存储用户会话信息(如 Spring Security 的 SecurityContextHolder)。
  • 数据库连接管理(如某些 ORM 框架用 ThreadLocal 绑定连接,确保同一事务使用同一连接)。

# 性能优化

场景:复用线程内频繁创建的高开销对象(如缓冲区)。

private static final ThreadLocal<ByteBuffer> buffer = 
    ThreadLocal.withInitial(() -> ByteBuffer.allocate(1024));
1
2

# ThreadLocal 的内存泄漏问题与解决方案

  1. 内存泄漏的根本原因
  • 数据结构:每个线程内部维护一个 ThreadLocalMap,其 Entry 的 Key 是 ThreadLocal 的弱引用,Value 是强引用。
  • 问题:
    • 如果 ThreadLocal 实例被回收(如置为 null),Key 变为 null,但 Value 仍被强引用,导致无法回收。
    • 线程池场景下,线程长期存活,累积的无效 Entry 会导致内存泄漏。

ThreadLocalMap中的key是弱引用,而value是强引用。

什么是强引用,什么是弱引用。记不清楚,就复习一遍。

  1. 解决方案
  • 强制调用 remove()

    :使用完 ThreadLocal 后,必须调用

    remove()
    
    1

    清理当前线程的 Entry。

    try {
        threadLocal.set(data);
        // ... 业务逻辑
    } finally {
        threadLocal.remove(); // 确保清理
    }
    
    1
    2
    3
    4
    5
    6
  • 避免使用全局静态 ThreadLocal: 非静态的 ThreadLocal 实例应随着对象的生命周期结束而自然释放。

# 一个例子-示例代码

# 场景:模拟用户登录信息存储

public class UserContext {
    // 定义 ThreadLocal(推荐静态 final)
    private static final ThreadLocal<User> currentUser = new ThreadLocal<>();

    // 设置当前用户
    public static void setUser(User user) {
        currentUser.set(user);
    }

    // 获取当前用户
    public static User getUser() {
        return currentUser.get();
    }

    // 清理用户信息
    public static void clear() {
        currentUser.remove(); // 必须手动清理
    }

    // 用户类
    public static class User {
        private final String name;
        public User(String name) { this.name = name; }
        public String getName() { return name; }
    }

    public static void main(String[] args) {
        // 模拟请求处理
        Runnable task = () -> {
            try {
                // 1. 设置当前用户
                setUser(new User(Thread.currentThread().getName()));
                
                // 2. 执行业务逻辑
                System.out.println("当前用户: " + getUser().getName());
                
                // 3. 模拟后续操作(可能调用其他方法)
            } finally {
                // 4. 强制清理,避免内存泄漏
                clear();
            }
        };

        // 启动两个线程
        new Thread(task).start(); // 输出:当前用户: Thread-0
        new Thread(task).start(); // 输出:当前用户: Thread-1
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 代码解析

  1. ThreadLocal 定义: currentUser 是静态 final 变量,确保全局唯一性。
  2. 资源绑定与清理:
    • setUser():将用户信息绑定到当前线程。
    • getUser():从当前线程获取用户信息。
    • clear():在 finally 块中调用 remove(),确保线程池中线程重用时不会残留旧数据。
  3. 内存泄漏防御: finally 块中的 clear() 是关键,即使业务代码抛出异常,也能保证清理。

# JMM的三大特性

Java 内存模型(JMM)中,原子性、可见性、有序性 是并发编程的核心问题。

# 三大特性的诞生

现代计算机的硬件架构(如 CPU 缓存、多核 CPU、指令重排序优化)导致以下问题:

  • 可见性问题:一个线程对共享变量的修改,其他线程可能无法立即看到。
  • 有序性问题:编译器或 CPU 可能对指令进行重排序优化,导致代码执行顺序与预期不符。

现代计算机系统通常拥有多个层级的缓存(L1, L2, L3等),为了提高性能,每个CPU核心都有自己的缓存。当一个线程在某个核心上修改了一个共享变量时,这个修改首先会反映在其本地缓存中,而不会立即写回到主内存。这就可能导致运行在不同核心上的其他线程无法立即看到这个更新,因为它们访问的是自己核心上的缓存副本。所以说,多级缓存导致了可见性问题。

而有序性问题则是因为,提高多核cpu的性能而导致的。

比如CPU-B想要读取数据D的时候,还需要等待CPU-A将数据D写回主存,那么这种行为是难以忍受的,因此计算机科学家们做了一些优化,怎么优化呢?整体思想上就是将同步改成异步。比如CPU-B想要读取数据D的时候,发现D正在被其他的CPU修改。那么此时CPU-B可以注册一个读取D的消息,自己回头去做其他事情。其他CPU写回数据D后,响应了这个注册消息。此时CPU-B发现消息被响应后,再去读取地,这样就能够有效的提升效率。但是对于CPU-B来说,程序看上去就不是顺序执行的了,可能会出现先运行后面的指令,再返回头去运行前面的指令这种行为。这就体现出了一种指令重排序。

而原子性则是因为机器本身的指令导致的。因为有些功能可能需要多个指令才能实现,这里就不展开了。你只需要记住,原子性指一个操作或多个操作要么全部执行且不被打断,要么完全不执行。若某个操作需要多个步骤完成,且这些步骤可能被线程切换打断,则称为非原子操作。

JMM 定义了线程与主内存交互的原子操作(如 read、load、use、assign、store、write),并规定这些操作必须按顺序成对出现。

计算机科学家们,设计了硬件内存模型,其目标是为了让汇编代码能够运行在一个具有一致性的内存视图上。保证数据的一致性。

# JMM的诞生

随着高级语言的流行,工程师们开始设计编程语言级别的内存模型。这是为了能够使用该语言进行编程的时候,也能拥有一个一致性的内存视图。于是在硬件内存模型之上,还存在着为编程语言设计的内存模型。比如Java内存模型就屏蔽掉了各种硬件和操作系统的内存访问差异,实现了让java程序能够在各种硬件平台下都能够按照预期的方式来运行。

也就是说JMM是为了保证在各个硬件平台上(各个操作系统上),在保证数据一致的情况下运行,或者说Java内存模型是对物理内存的一种模拟。

现在你能回答为什么,需要定义JMM(Java内存模型或者说 线程模型)了吗?

答:我们在开发时如果直接调用操作系统的接口来创建和回收线程,不是更加直接吗?这个问题的答案其实很容易理解,就像我们现在为什么不常用汇编语言来进行开发,而是使用更加简单,更加容易上手的高级语言一样。这是一种自下而上的抽象方式。JVM线程的对不同操作系统的原生线程进行了高级抽象,使开发者一般情况下可以不用关注下层的细节,而只要专注上层的开发就行了。但是在学习过程中,我们需要秉持知其然并知其所以然的态度,就要去理解这种抽象方式。这也有助于我们在将来自己进行一些设计的时候,能够调用前人的思想理解了什么是线程模型,为什么要线程模型。

以下是它们的一些介绍及如何通过 synchronized 和 volatile 保证的详细分析:

# 原子性(Atomicity)

定义:一个操作不可被中断,要么全部执行成功,要么完全不执行。

非原子操作示例(如 i++):

public class AtomicityExample {
    private int count = 0;

    public void increment() {
        count++; // 非原子操作(实际是三步:读值、+1、写回)
    }
}
1
2
3
4
5
6
7

多线程调用 increment() 会导致结果不一致(如两个线程同时读 count=5,最终结果可能是 6 而非 7)。

# 保证原子性

  • 使用synchronized,加锁

  • 使用原子类

使用 AtomicInteger 等原子类(底层基于 CAS):

private AtomicInteger count = new AtomicInteger(0);
public void increment() {
    count.incrementAndGet();
}
1
2
3
4

volatile 不能保证原子性,

它只能保证单次读/写的原子性(如 volatile int x = 10 的赋值是原子的),但 x++ 仍是非原子的。

# 可见性(Visibility)

定义

一个线程修改共享变量后,其他线程能立即看到修改后的值。

可见性问题代码示例:

public class VisibilityExample {
    private boolean flag = false; // 非 volatile

    public void writer() {
        flag = true; // 线程 A 修改
    }

    public void reader() {
        while (!flag); // 线程 B 可能永远无法退出循环
        System.out.println("Flag is now true");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

由于线程 B 的本地缓存未感知到 flag 的修改,导致死循环。

# 保证可见性

使用 volatile 关键字,强制每次读写直接操作主内存,禁用本地缓存:

private volatile boolean flag = false;
1

或者通过加锁,synchronized,的方式实现

# 有序性(Ordering)

定义

程序执行的顺序按照代码的先后顺序执行(禁止指令重排序)。

代码体现

指令重排序导致的问题(双重检查锁的单例模式):

public class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查
                    instance = new Singleton(); // 可能发生指令重排序
                }
            }
        }
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

new Singleton() 的指令可能被重排序为:

  1. 分配内存空间
  2. 将引用指向内存(此时 instance != null)
  3. 初始化对象 若线程 A 执行到第 2 步时,线程 B 可能拿到未初始化的 instance。

# 保证有序性

volatile

禁止 JVM 对 volatile 变量的读写指令进行重排序:

private static volatile Singleton instance; // 修复双重检查锁问题
1

synchronized

通过锁的互斥性保证临界区内代码的顺序执行(但锁外代码仍可能被重排序)。

# 加锁过程是如何保证3个特性的

# volatile的实现

那么volatile是如何保证可见性跟指令重排的。

Volatile关键字下层的实现保证了若一个被volatile修饰的变量被修改,那么总是会主动的写入内存。若要读取一个被volatile修饰的变量,那么总是从主存中读取。这样的话相当于操作被volatile修饰的变量时,都是直接去读写主存,这样就能够解决上面的可见性问题。

而指令重排,则是volatile是因为内部做了内存屏障所以让程序运行到有volatile关键字修饰的变量操作时强制顺序执行,而不会出现指令重排序的情况。

# synchronized的实现

关于原子性的实现:

  • 互斥性(Mutual Exclusion):锁确保同一时刻只有一个线程能进入临界区(Critical Section),操作共享资源时不会被打断。
  • 临界区内的操作不可分割:线程在临界区内的所有操作会被视为一个原子操作。

关于可见性的实现:

在同步代码块中,在monitor的基础上当读写变量时,会隐式的执行上文提到的内存lock指令,并清空工作内存中该。变量的值需要使用该变量时,必须从主存中获取。同理也会隐式的执行内存unlock指令,将修改过的变量刷新回主存,这样就也能够解决可见性问题。

总结下就是:

  • 锁的获取(Acquire):线程进入 synchronized 块或调用 lock() 时,会强制从主内存重新加载共享变量的最新值(清空工作内存的旧值)。
  • 锁的释放(Release):线程退出 synchronized 块或调用 unlock() 时,会将工作内存中对共享变量的修改强制刷新到主内存。

锁通过插入 内存屏障 实现以下效果:

  • 禁止重排序:临界区内的代码不会被编译器或 CPU 重排序到临界区外。
  • 强制同步:确保共享变量的修改对其他线程立即可见。

关于原子性的实现:

  • 程序顺序规则:单线程内,临界区内的代码顺序与程序代码顺序一致(编译器/CPU 不会对临界区内的代码进行破坏语义的重排序)。
  • Happens-Before 规则:
    • 锁的释放 Happens-Before 后续锁的获取:线程 A 释放锁后,线程 B 获取锁时能看到线程 A 的所有修改。
    • 临界区内的操作不会被重排序到临界区外。

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待

上次更新: 2025/04/23, 16:23:16
并发包中的AQS
锁的探究

← 并发包中的AQS 锁的探究→

最近更新
01
面向切面跟自定义注解的结合
05-22
02
时间跟其他数据的序列化
05-19
03
数据加密与安全
05-17
更多文章>
Theme by Vdoing | Copyright © 2023-2025 EffectTang
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式