目录

第一章:多线程初体验与核心概念

第二章:创建线程的三大门派

第三章:线程的常用方法与“特权阶级”

第四章:线程的生命周期与 6 种状态

第五章:万恶之源 —— 线程安全与锁

3. 悲观锁 (Pessimistic Lock) —— “总有刁民想害朕”

4. 乐观锁 (Optimistic Lock) —— “佛系打工人”

第六章:等待唤醒机制 (生产者与消费者模式)

第七章:多线程综合练习 —— 抢红包算法

完整代码与操作说明 (RedPacketDemo.java)

第八章:终极杀器 —— 线程池 (ThreadPoolExecutor)

第九章:JUC 额外扩展内容


哈喽,各位还在和 Bug 斗智斗勇的码农战友们!

如果说之前学的面向对象、集合、IO 流是在教你怎么“单干”,那么今天我们要触碰的 多线程与 JUC (Java Util Concurrent),就是教你学会“分身术”,让你的代码开启狂暴模式。

在大厂面试中,多线程和 JUC 绝对是“重灾区”和“造火箭”的核心考点。知识点极密、极度烧脑!别慌,今天我带你喝着茶,把这块硬骨头一点点嚼碎!


第一章:多线程初体验与核心概念

1. 什么是多线程? 想象你在打游戏(这是一个进程),游戏里既有背景音乐在播放,又有怪物在移动,你还能同时放技能。这背后就是多个“线程”在同时干活。进程是操作系统分配资源的最小单位,而线程是 CPU 调度的最小单位。

2. 并发 (Concurrency) vs 并行 (Parallelism) 这俩词面试必问!

  • 并发: 一个人同时对付两个相亲对象。单核 CPU 在多个线程之间极速来回切换,因为切换得太快,你感觉它们是“同时”在运行。(交替执行)

  • 并行: 两个人分别对付两个相亲对象。多核 CPU 真正意义上在同一时刻,同时执行多个线程。(同时执行)


第二章:创建线程的三大门派

在 Java 中搞出分身,有三种经典套路:

  1. 第一种:继承 Thread 类 (最简单,但不推荐)

    • 缺点: Java 是单继承,你继承了 Thread,就不能继承别的类了,相当于自绝后路。

  2. 第二种:实现 Runnable 接口 (最常用,打工人必备)

    • 优点: 只是实现一个接口,还可以继续继承别的类,扩展性极好。我们经常把 Runnable 当作一个“任务”扔给线程去跑。

  3. 第三种:实现 Callable<T> 接口 + FutureTask (高端操作,能拿返回值)

    • 场景: 如果你想让子线程去数据库查个数据,并且主线程需要拿到这个查出来的结果,或者想捕获子线程抛出的异常,就必须用它!


第三章:线程的常用方法与“特权阶级”

1. 常用成员方法

  • String getName() / setName(String name):获取/设置线程名字。

  • static Thread currentThread():极其重要!获取当前正在执行代码的那个线程对象。

  • static void sleep(long millis):让当前线程睡一会儿(毫秒),注意:睡着的时候不会释放锁!

2. 线程的优先级 (Priority)

  • Java 中优先级分为 1~10,默认是 5。

  • 真相: 优先级高只是意味着抢到 CPU 时间片的概率变大了,并不保证一定先执行。不要过度依赖它来实现业务逻辑。

3. 守护线程 (Daemon Thread) —— “舔狗”线程

  • 概念: 当程序中所有的“非守护线程(比如主线程)”都执行完毕死掉后,守护线程哪怕活没干完,也会被系统强制殉情,直接结束。

  • 代表作: Java 的垃圾回收器 (GC) 就是最经典的守护线程。

4. 礼让线程 (yield) 与 插入线程 (join)

  • 礼让: Thread.yield()。当前线程主动让出 CPU 执行权,重新回炉和其他线程一起抢。(只是客气一下,下一次可能还是它抢到)。

  • 插队: t.join()。霸道总裁!一旦调用,当前线程必须停下来,死等 t 线程执行完毕,自己才能继续往下走。


第四章:线程的生命周期与 6 种状态

面试官最爱让你在黑板上画这张图。Java 在 Thread.State 枚举中定义了 6 种状态:

  1. NEW (新建):new 出来,还没调 start()

  2. RUNNABLE (可运行): 调了 start(),可能正在跑,也可能在等 CPU 翻牌子。

  3. BLOCKED (阻塞): 想要获取锁进门,但锁被别人拿了,只能在门外干瞪眼。

  4. WAITING (无限等待): 调用了 wait(),没有期限,除非别人调 notify() 叫醒它,否则等到海枯石烂。

  5. TIMED_WAITING (计时等待): 调用了 sleep(1000)wait(1000),设了闹钟,时间到了自己醒。

  6. TERMINATED (终止): run() 方法代码执行完毕,安详离世。


第五章:万恶之源 —— 线程安全与锁

当多个线程同时修改同一个共享资源(比如卖同一堆票,扣同一个账户的钱)时,就会发生数据错乱。

1. 解决方案:加锁!

  • 同步代码块 (synchronized(锁对象) { ... }): 精准打击,只锁住容易出事的核心代码。锁对象必须是所有线程公用的同一个对象!

  • 同步方法 (public synchronized void doSomething()): 锁住整个方法。普通方法的隐形锁是 this,静态方法的隐形锁是 当前类的.class字节码对象

  • Lock 锁 (ReentrantLock): JUC 包下的专业电子锁。需要手动 lock() 上锁,并且千万切记要在 finally 块中手动 unlock() 解锁!

2. 死锁 (Deadlock)

  • 现象: 线程 A 拿着锁 1 等锁 2,线程 B 拿着锁 2 等锁 1。两个人都寸步不让,导致程序永远卡死。

  • 预防: 尽量不要让锁嵌套发生(也就是不要在 synchronized 里面再套一层不同对象的 synchronized)。

3. 悲观锁 (Pessimistic Lock) —— “总有刁民想害朕”

(1). 核心思想

悲观锁,人如其名,它对这个世界充满了极度的不信任。 它认为:只要我把数据放出去,就一定会有其他线程来捣乱修改! 所以它的处理方式极其霸道:无论你是来读数据的还是写数据的,只要我碰了这个数据,我就立马给它上一把死锁。其他人全都在门外给我乖乖排队挂起(阻塞),等我干完活松开手,你们才能进来。

(2). Java 中的代表人物

  • 我们前面学的 synchronized 关键字

  • JUC 包下的 ReentrantLock

(3). 优缺点与适用场景

  • 优点: 绝对的安全,数据绝不会错乱。

  • 缺点: 极度消耗性能!线程排队、阻塞等待、以及 CPU 频繁地把线程唤醒和挂起(上下文切换),这可是非常耗费系统资源的操作。

  • 适用场景: 写多读少的场景。既然大家都是来改数据的,冲突概率极高,不如直接排队。

4. 乐观锁 (Optimistic Lock) —— “佛系打工人”

(1). 核心思想 乐观锁的心态极其阳光。它认为:哪怕有成千上万的并发,大家一起修改同一个数据的概率也很低。

所以它的处理方式非常开放:我不加锁! 大家随便来拿数据,随便在自己的内存里修改。但是! 在你准备把修改后的结果写回主内存的最后一刻,系统会进行一次极其严格的校验

  • 如果在此期间,没有别人动过这个数据,你的修改就成功了。

  • 如果发现这期间别人已经捷足先登把数据改了,那么你的修改作废。你需要重新把最新的数据读取过来,再次计算,再次尝试提交(这个过程叫自旋重试)。

(2). 核心底层技术:CAS (Compare And Swap - 比较并交换) 乐观锁能在不加锁的情况下保证安全,全靠 CPU 硬件级别支持的 CAS 指令。 CAS 包含三个关键值:

  • V (内存实际值): 此时此刻主内存里真实的数据。

  • E (预期旧值): 你当初把数据拿过来准备修改时,看到的值。

  • N (新值): 你计算完之后,想要写回去的值。

执行逻辑: 当且仅当 V == E 的时候(说明期间没人动过数据),才会把 V 更新为 N。如果不等,就不断循环重试,直到成功为止。

(3). Java 中的代表人物

  • JUC 包下的所有原子类 (Atomic 开头的类),比如 AtomicInteger

(4). 优缺点与适用场景

  • 优点: 没有任何线程会被阻塞!大家都在飞速运转,省去了 CPU 切换线程的巨大开销,性能极高。

  • 缺点: 如果冲突真的极其频繁,大量线程会一直处于“修改失败 -> 重新读取 -> 再次失败”的死循环中(自旋),白白榨干 CPU 资源。

  • 适用场景: 读多写少的场景。偶尔发生冲突重试一下无伤大雅,绝大部分时间享受无锁的丝滑。


第六章:等待唤醒机制 (生产者与消费者模式)

有时候线程之间需要配合干活。比如:包子铺厨师(生产者)和吃货(消费者)。吃货发现没包子了,得等着(wait);厨师做好了,得叫醒吃货(notify)。

1. 传统写法:思路分析与 wait/notify 这套机制必须依赖同一个锁对象来实现。

  • 消费者拿到锁,判断有没有数据。有就消费,没有就调用 锁对象.wait() 陷入沉睡,并释放锁

  • 生产者拿到锁,生产数据,调用 锁对象.notifyAll() 唤醒所有正在等这把锁的消费者。

2. 降维打击:阻塞队列 (BlockingQueue) 传统写法需要手写大量锁和判断逻辑,极其容易写出死锁。大厂现在都在用 JUC 提供的 BlockingQueue(如 ArrayBlockingQueue)。

  • 它就像一个自带智能红绿灯的管道。

  • 生产者调 put() 往里塞数据,如果管子满了,生产者自动阻塞挂起。

  • 消费者调 take() 往外抽数据,如果管子空了,消费者自动阻塞挂起。

  • 精髓:synchronizedwait/notify 都不用写,底层全帮你封装好了!


第七章:多线程综合练习 —— 抢红包算法

我们来实战模拟一个经典场景:3个人(3个线程)去抢 100 块钱,一共分 3 个红包。要求抢完为止,且金额随机(简单版逻辑)。

完整代码与操作说明 (RedPacketDemo.java)
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Random;

public class RedPacketDemo {
    public static void main(String[] args) {
        // 创建一个红包任务
        RedPacketTask task = new RedPacketTask();

        // 创建三个线程代表三个人
        Thread t1 = new Thread(task, "张三");
        Thread t2 = new Thread(task, "李四");
        Thread t3 = new Thread(task, "王五");

        t1.start();
        t2.start();
        t3.start();
    }
}

// 实现 Runnable 接口
class RedPacketTask implements Runnable {
    // 共享数据:总金额 100.0,红包个数 3
    private double totalMoney = 100.0;
    private int count = 3;
    // 随机数生成器
    private Random random = new Random();
    // 锁对象
    private final Object lock = new Object();

    @Override
    public void run() {
        // 使用同步代码块保证线程安全
        synchronized (lock) {
            if (count == 0) {
                System.out.println(Thread.currentThread().getName() + ":来晚了,红包抢光了!");
            } else {
                double prize = 0;
                if (count == 1) {
                    // 如果是最后一个红包,直接包圆剩下的钱
                    prize = totalMoney;
                } else {
                    // 核心随机算法:每次最多抢剩下金额的 (平均值 * 2) 以内,防止第一个人把钱全抢光
                    // 这是微信红包的经典简单算法逻辑
                    double max = (totalMoney / count) * 2;
                    prize = random.nextDouble() * max;
                    // 防止抢到 0 元,保底 0.01
                    if (prize < 0.01) {
                        prize = 0.01;
                    }
                }

                // 扣除红包个数和金额
                count--;
                totalMoney -= prize;

                // 格式化金额,保留两位小数
                BigDecimal bd = new BigDecimal(prize);
                bd = bd.setScale(2, RoundingMode.HALF_UP);

                System.out.println(Thread.currentThread().getName() + " 抢到了红包:" + bd.doubleValue() + " 元");
            }
        }
    }
}

操作说明:

  1. 新建 RedPacketDemo.java 文件。

  2. 将上述代码完整复制并保存。

  3. 运行 main 方法。你会看到三个人随机分掉了这 100 元,且每次运行的分配结果都不一样,绝不会超额或者少发。


第八章:终极杀器 —— 线程池 (ThreadPoolExecutor)

在实际开发中,如果每次执行任务都去 new Thread(),会导致内存中存在成千上万个死亡线程的残骸,瞬间撑爆内存。 线程池的出现,就是为了让线程能够“复用”。 就像一个外包公司,养了一批固定的打工人,有活儿就接,干完不辞退,接着等下一个活儿。

1. 自定义线程池的 7 大核心参数 (面试必背,能默写的那种!)

ThreadPoolExecutor pool = new ThreadPoolExecutor(
    2,  // 1. corePoolSize: 核心线程数 (正式员工数量,哪怕闲着也不辞退)
    5,  // 2. maximumPoolSize: 最大线程数 (正式员工 + 临时工 的总和)
    2,  // 3. keepAliveTime: 临时工摸鱼存活时间
    TimeUnit.SECONDS, // 4. unit: 存活时间的单位
    new ArrayBlockingQueue<>(10), // 5. workQueue: 阻塞队列 (客户排队的候客区)
    Executors.defaultThreadFactory(), // 6. threadFactory: 线程工厂 (决定怎么招人)
    new ThreadPoolExecutor.AbortPolicy() // 7. handler: 拒绝策略 (候客区满了,临时工也招满了,再来新活儿怎么处理)
);

2. 灵魂拷问:最大并行数是多少?线程池到底设多大合适?

  • 最大并行数: 取决于你电脑的 CPU 核心数(可以通过 Runtime.getRuntime().availableProcessors() 获取)。

  • 池子大小公式(行业经验法则):

    • CPU 密集型任务(比如大量计算): 设为 最大核心数 + 1。榨干 CPU 性能,多出来的 1 个用来顶替偶尔缺勤的线程。

    • IO 密集型任务(比如读写文件、查数据库): 设为 最大核心数 * 2。因为 IO 操作很慢,CPU 会大量闲置,多开点线程可以提高 CPU 利用率。


第九章:JUC 额外扩展内容

JUC (java.util.concurrent) 是 Java 并发编程的宝库,除了上面学的,还有几个明星产品:

  • Volatile 关键字: 保证变量在多个线程之间的“可见性”,并禁止指令重排。但它不保证原子性(不能代替锁)。

  • CAS 算法与原子类 (AtomicInteger): 乐观锁机制,不加锁也能保证线程安全,性能极高。

  • ConcurrentHashMap 线程安全版本的 HashMap。多线程环境下千万别用普通的 HashMap,死循环警告!

  • CountDownLatch (倒计时器): 比如让主线程等 5 个子线程全把活儿干完了,主线程再继续往下走。


从线程基础到并发锁,再到工业级的线程池架构,恭喜你已经啃下了 Java 最硬的一块骨头。

前面提到过很多次“网络读取”,你想接着学习两台电脑之间是如何通过网络互传数据的 网络编程 (Socket & TCP/UDP) 吗?那可是后续微服务通信的基石!

如果我的内容对你有帮助,请点赞,评论,收藏。接下来我将继续更新相关内容!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐