Java并发与基础-每日必看(10)

前言

Java不秃,面试不慌!
欢迎来到这片 Java修炼场!这里没有枯燥的教科书,只有每日一更的 硬核知识+幽默吐槽,让你在欢笑中掌握 Java基础、算法、面试套路,摆脱“写代码如写诗、看代码如看天书”的困境。
记住: 代码会背叛你,但知识不会! 坚持积累,总有一天,HR会为你的八股文落泪,面试官会因你的算法沉默。


ThreadLocal的底层原理

    想象一下,你的公司有一台超级复印机,每个员工(线程)都可以用它来存放自己的小笔记本数据)。但是,每个人的笔记本都不一样,而且只有自己能翻阅,其他人完全看不到。这台神奇的复印机就是 ThreadLocal,它能确保你的笔记本不会被其他员工偷看或误用。

1. 线程自己的小秘密

ThreadLocal 是 Java 提供的一种 线程本地存储 机制,简单来说,它允许你在一个线程内部存放自己的数据,而 不和别的线程分享。就像每个人都有自己的私人笔记本,里面记着自己专属的小秘密,别人无法偷看。

2. ThreadLocalMap:藏在线程里的私密抽屉

ThreadLocal 并不是一个存储数据的仓库,而是一个钥匙(key),它的核心秘密藏在 ThreadLocalMap 里面。

每个 Thread(线程) 都有一个自己的 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 对象value 是你想存的数据,大致结构如下:

class Thread {
    ThreadLocalMap threadLocals; // 每个线程都有一个 ThreadLocalMap
}

class ThreadLocal {
    public void set(Object value) {
        Thread currentThread = Thread.currentThread();
        currentThread.threadLocals.put(this, value);
    }

    public Object get() {
        Thread currentThread = Thread.currentThread();
        return currentThread.threadLocals.get(this);
    }
}

说白了,每个线程都带着自己的小抽屉(ThreadLocalMap),这个抽屉里放着你设置的东西(value),别人看不见,拿不到,甚至不知道你抽屉里有啥。


ThreadLocal 在线程池里用,会有内存泄漏?

问题:你的垃圾还在你桌子上

如果你用的是 线程池,就会遇到一个问题:线程不会被回收,但你的数据还留在 ThreadLocalMap 里,导致 内存泄漏

这就像你的公司为了节约资源,员工(线程)不会辞职(销毁),而你用完的笔记本(数据)也没扔,桌子上的东西(ThreadLocalMap)越积越多,最后连自己都找不到键盘在哪儿了。

为什么会这样?

  1. ThreadLocalMap 是线程的一个属性,它被线程对象(Thread)强引用
  2. Entry(键值对)里面的 key 是 ThreadLocal 的弱引用,但 value 仍然是强引用。
  3. 如果 ThreadLocal 对象被 GC 回收了,key 变成 null 了,但是 value 还留在 ThreadLocalMap 里,导致内存泄漏
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("秘密数据");

// 这里 threadLocal 变量被置为 null,但数据还在 ThreadLocalMap 里
threadLocal = null;

// 线程不会马上结束,ThreadLocalMap 里的数据就一直在,导致泄漏!

解决方案

👉 用完一定要手动清理
用完 ThreadLocal 之后,记得 调用 remove() 方法,清空数据,避免内存泄漏。

threadLocal.remove(); // 该扔的垃圾还是得扔

经典应用场景:数据库连接管理

想象你在餐厅吃饭(一个请求),你需要一套独立的餐具(数据库连接)。每个顾客(线程)都有自己的一套餐具,不会和别人共用,否则容易吃到别人的口水(数据错乱)。

ThreadLocal 可以完美解决这个问题!
每个线程都维护自己的数据库连接,而不是所有线程共用一个连接池,这样就不会产生并发问题。

public class DBConnectionManager {
    private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();

    public static Connection getConnection() {
        if (connectionHolder.get() == null) {
            connectionHolder.set(createNewConnection());
        }
        return connectionHolder.get();
    }

    public static void closeConnection() {
        try {
            if (connectionHolder.get() != null) {
                connectionHolder.get().close();
                connectionHolder.remove();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

这个逻辑就像你在吃饭的时候,侍应生会在你的座位上放一套餐具(数据库连接),你吃完饭(请求完成)之后,餐具会被收走(remove),不会影响下一位顾客。

总结

  • ThreadLocal 是一个线程局部变量存储机制,让数据只在当前线程可见,线程之间不会相互干扰。
  • 底层是通过 ThreadLocalMap 来实现,每个线程都持有一个自己的 Map,存储着不同的 ThreadLocal 变量。
  • 在线程池中使用 ThreadLocal 可能会导致内存泄漏,因为线程不会被销毁,ThreadLocalMap 里的数据不会自动清理。
  • 使用完 ThreadLocal 后,一定要调用 remove() 方法,否则会有内存泄漏问题。
  • 最经典的应用场景是数据库连接管理,确保每个线程持有自己的数据库连接,避免多个线程共用同一个连接导致数据错乱。

并发、并⾏、串⾏之间的区别:

想象你在一个餐厅里点餐,餐厅的运作方式可以有三种:串行、并发、并行

1. 串行(Serial):排队等候,先来后到

就像你去一个只有 一个厨师 的小饭馆,他每次只能做一道菜。

  • 你点了炒饭,厨师开始炒,别的菜只能等他炒完再做。
  • 等炒饭做完,他才去做下一道菜,比如宫保鸡丁。
  • 如果有10道菜,那就只能一步一步来,没有任何重叠。

特点:任务必须一个接一个地完成,不能同时进行。

void serialExecution() {
    taskA(); // 先执行任务A
    taskB(); // A做完后再执行B
}

2. 并行(Parallel):多个任务同时进行,互不干扰

这就像一个大厨房里有 多个厨师,每个人可以独立做不同的菜。

  • 厨师A炒饭,厨师B炸鸡,厨师C煮汤,大家互不影响,一起忙活
  • 这样所有菜都能同时出锅,速度比串行快多了。

特点:多个任务在同一时间真正“同时”运行,彼此不会干扰。
(需要多核CPU支持,每个任务分配到不同的CPU核心上执行)

void parallelExecution() {
    new Thread(this::taskA).start(); // 任务A在一个线程中运行
    new Thread(this::taskB).start(); // 任务B在另一个线程中运行
}

3. 并发(Concurrent):快速切换,制造“同时运行”的假象

这就像你去了一家只有 一个厨师 但又很聪明的饭馆。

  • 他炒饭炒一半,觉得油温还没到,于是先去切宫保鸡丁的肉。
  • 肉切了一半,听到水开了,又去煮汤。
  • 他并不是同时做这些菜,而是不断切换,让每道菜都“看起来”在做。

特点:虽然看似多个任务在一起执行,但其实是“交替”运行的,真正的同时进行并不多。
(单核CPU也能支持并发,因为它通过时间片轮转快速切换任务)

void concurrentExecution() {
    ExecutorService executor = Executors.newFixedThreadPool(1); // 只有一个线程
    executor.submit(this::taskA); // 先执行任务A
    executor.submit(this::taskB); // 任务A没执行完,切换到任务B
    executor.shutdown();
}

并发的三大特性

想象你在一个银行里转账、查账、存款,银行的系统必须保证你的数据不会被乱改、不会看错余额、不会因为服务器优化导致计算错误。这背后就涉及 并发的三大特性原子性、可见性、有序性


1. 原子性(Atomicity):要么全做,要么不做

你去银行给朋友转 1000 块钱,这个操作一定包含两个步骤:

  1. 从你的账户里扣掉 1000 块
  2. 给朋友的账户里加上 1000 块

如果这两个操作 不能保证原子性,可能发生这样离谱的情况:

  • 银行先扣了你的钱,然后突然服务器崩溃了,朋友那边的钱还没到账!
  • 又或者,你的账户钱已经扣了,朋友的账户还没加上,结果另一个线程查到账户余额时发现钱没了,但实际上钱根本没真正转出去

所以 要么两步都完成,要么一步都不做,这就是 原子性 的保证。

但有些看似很简单的操作,比如自增 i++,其实并不是原子性的!

i++  // 实际上分成了三步:
1. 读取变量 i
2. 让 i 加 1
3. 把 i 写回内存

如果两个线程同时执行 i++,可能出现数据丢失的问题。

如何保证原子性?

  • synchronized:加锁,确保多个线程不会同时执行某段代码。
  • AtomicInteger:使用 Java 提供的 原子类 让自增变成原子操作。
  • Lock:使用 ReentrantLock 显式控制锁的获取和释放。

示例:用 synchronized 让 i++ 变成原子操作

private int count = 0;

public synchronized void increment() {
    count++;
}

这样,即使多个线程同时执行 increment(),它们也必须一个个来,保证不会出错。


2. 可见性(Visibility):你的修改,别人必须立刻能看到

问题: 你去 ATM 机存了 500 块,但你打开手机银行,余额竟然还是之前的数字?!

这就是 可见性问题——你的改动没有及时同步到“主存”(共享数据区),导致其他线程(手机银行)看不到你存的钱。

在 Java 中,每个线程都有自己的工作内存,它可能会缓存变量的值,但不一定立刻同步到主存。例如:

// 线程1
boolean stop = false;
while (!stop) {
    doSomething();
}

// 线程2
stop = true; // 线程2修改了 stop

问题: stop = true 一定能让线程1停下来吗?
不一定! 因为 stop 可能还停留在线程2的本地缓存,线程1 根本没看到 stop 变成 true,所以它会一直循环不停。

如何保证可见性?

  • volatile 关键字:强制线程每次都从主存读取变量,而不是用自己的缓存。
  • synchronized:保证锁的释放时会把最新数据写回主存,并让其他线程立刻看到变化。
  • final:在对象初始化完成后,确保值不会再变。

示例:用 volatile 解决可见性问题

volatile boolean stop = false;

public void run() {
    while (!stop) {
        doSomething();
    }
}

public void stopThread() {
    stop = true; // 线程1能立刻看到这个变化
}

volatile 让 stop 直接同步到主存,这样线程1立刻就能看到变化,避免死循环。


3. 有序性(Ordering):代码可能不是按你写的顺序执行

你写代码的顺序,不代表 CPU 就会按这个顺序执行

Java 为了优化性能,可能会对代码进行 指令重排序(reordering)。在单线程下,这种优化通常不会有问题,但在多线程下,它可能导致严重的错误。

示例:

int a = 0;
boolean flag = false;

public void write() {
    a = 2; // (1)
    flag = true; // (2)
}

public void multiply() {
    if (flag) { // (3)
        int ret = a * a; // (4)
    }
}

可能发生的问题

  • 你以为 a=2 先执行,然后 flag=true 才执行。
  • 但 Java 可能会优化,把 (2) flag = true; 提前执行
  • 这样,如果 multiply() 先看到 flagtrue,但 a 可能还没赋值,导致计算 a * a 出错。

如何保证有序性?

  • volatilevolatile 禁止指令重排序,确保 a=2 一定先执行,再执行 flag=true
  • synchronized:线程进入同步块时,会刷新变量值,保证指令按顺序执行。

示例:用 volatile 防止重排序

volatile int a = 0;
volatile boolean flag = false;

public void write() {
    a = 2;
    flag = true; // 这里不会被提前执行
}

public void multiply() {
    if (flag) {
        int ret = a * a; // 现在一定是 2 * 2
    }
}

总结

特性定义问题举例解决方案
原子性要么全做,要么不做i++ 在多线程下可能丢失更新synchronizedAtomicInteger
可见性修改的数据,其他线程能立刻看到线程1修改变量,线程2读取到的还是旧值volatilesynchronized
有序性代码执行顺序和编写顺序可能不一样a=2; flag=true; 可能被重排序导致错误volatilesynchronized

最终结论:

  • synchronized 同时满足 原子性、可见性、有序性,但性能开销较大
  • volatile 只能保证可见性和有序性不保证原子性(比如 i++ 仍然不是原子操作)。
  • 如果volatile 能满足需求,就用volatile,否则用 synchronizedLock

最后的话:关注不迷路,点赞不迟到! 🎉

如果这篇文章让你对 并发三大特性(原子性、可见性、有序性) 有了更清晰的理解,记得给个 点赞 👍、收藏 ⭐、关注 ❤️,这样下次学习并发的时候就不会迷路啦!

你的 点赞关注,就是对我最大的支持,让我更有动力继续为大家输出 通俗易懂、幽默风趣的技术干货! 🚀

如果你有任何疑问或者更好的理解方式,欢迎 评论区交流,让我们一起 成为高并发高手! 💪

感谢阅读,我们 下篇文章见! 🎊

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Starry-Walker

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

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

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

打赏作者

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

抵扣说明:

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

余额充值