【从零开始java学习|第二十八篇】多线程&JUC :并发编程的终极奥义
目录
3. 悲观锁 (Pessimistic Lock) —— “总有刁民想害朕”
4. 乐观锁 (Optimistic Lock) —— “佛系打工人”
完整代码与操作说明 (RedPacketDemo.java)
第八章:终极杀器 —— 线程池 (ThreadPoolExecutor)
哈喽,各位还在和 Bug 斗智斗勇的码农战友们!
如果说之前学的面向对象、集合、IO 流是在教你怎么“单干”,那么今天我们要触碰的 多线程与 JUC (Java Util Concurrent),就是教你学会“分身术”,让你的代码开启狂暴模式。
在大厂面试中,多线程和 JUC 绝对是“重灾区”和“造火箭”的核心考点。知识点极密、极度烧脑!别慌,今天我带你喝着茶,把这块硬骨头一点点嚼碎!
第一章:多线程初体验与核心概念
1. 什么是多线程? 想象你在打游戏(这是一个进程),游戏里既有背景音乐在播放,又有怪物在移动,你还能同时放技能。这背后就是多个“线程”在同时干活。进程是操作系统分配资源的最小单位,而线程是 CPU 调度的最小单位。
2. 并发 (Concurrency) vs 并行 (Parallelism) 这俩词面试必问!
-
并发: 一个人同时对付两个相亲对象。单核 CPU 在多个线程之间极速来回切换,因为切换得太快,你感觉它们是“同时”在运行。(交替执行)
-
并行: 两个人分别对付两个相亲对象。多核 CPU 真正意义上在同一时刻,同时执行多个线程。(同时执行)
第二章:创建线程的三大门派
在 Java 中搞出分身,有三种经典套路:
-
第一种:继承
Thread类 (最简单,但不推荐)-
缺点: Java 是单继承,你继承了
Thread,就不能继承别的类了,相当于自绝后路。
-
-
第二种:实现
Runnable接口 (最常用,打工人必备)-
优点: 只是实现一个接口,还可以继续继承别的类,扩展性极好。我们经常把
Runnable当作一个“任务”扔给线程去跑。
-
-
第三种:实现
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 种状态:
-
NEW (新建): 刚
new出来,还没调start()。 -
RUNNABLE (可运行): 调了
start(),可能正在跑,也可能在等 CPU 翻牌子。 -
BLOCKED (阻塞): 想要获取锁进门,但锁被别人拿了,只能在门外干瞪眼。
-
WAITING (无限等待): 调用了
wait(),没有期限,除非别人调notify()叫醒它,否则等到海枯石烂。 -
TIMED_WAITING (计时等待): 调用了
sleep(1000)或wait(1000),设了闹钟,时间到了自己醒。 -
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(): 往外抽数据,如果管子空了,消费者自动阻塞挂起。 -
精髓: 连
synchronized和wait/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() + " 元");
}
}
}
}
操作说明:
-
新建
RedPacketDemo.java文件。 -
将上述代码完整复制并保存。
-
运行
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) 吗?那可是后续微服务通信的基石!
如果我的内容对你有帮助,请点赞,评论,收藏。接下来我将继续更新相关内容!
更多推荐

所有评论(0)