Java 王者修炼手册【并发篇 - 并发基础】:从线程状态到同步机制的底层修炼

大家好,我是程序员强子。
又来刷英雄熟练度咯~今天专攻 Java 并发基础,必须打牢地基!
之前练的 ConcurrentHashMap 底层涉及到非常多的并发知识,并没有把细节展开,今天开始准备深入啦~
并发基础直接是整个多线程战场的 通用基础,是后续啃 JUC、分布式锁、框架并发处理的打底子内容,练不扎实,高阶操作全是空中楼阁!
我们来看一下,今晚我们准备练习哪些内容:
线程状态相关:有哪些状态?状态之间如何转换?状态之间的区别?
多线程****任务载体:Thread 类和 Runnable ,Callable 与 Runnable 区别,FutureTask 作用?
通信协作相关:wait ()、notify ()、notifyAll () 、join ()等核心原理?线程同步有哪些方式?
线程安全****与切换:线程不安全是指什么?本质是什么?什么是线程上下文切换?发生在哪些场景?开销体现在哪些方面?如何量化?频繁上下文切换对系统性能有什么影响?如何减少上下文切换?
线程状态
有哪些状态?
NEW:刚创建还没调用 start();
RUNNABLE:调用 start()后进入,可能在运行中状态,也能在 就绪状态 ,JVM 不区分这俩,因为 CPU 调度权在操作系统手里,JVM 只认 **能被调度 **这个状态;
BLOCKED:
等待 synchronized 锁的释放,是 被动等待
没有持有任何锁(它是在抢锁的路上被拦住了)
持有锁的线程释放锁(比如退出 synchronized 块),此时 JVM 会从 BLOCKED 队列里选一个线程唤醒,让它去抢锁
WAITING:
主动调用 wait()/join()后 挂机等信号;
是 无限期等待(没超时,不被唤醒就永远等)
触发场景
object.wait(),等 notify()/notifyAll()唤醒
thread.join(),等线程执行完(底层是线程终止时自动调用 notifyAll())
LockSupport.park(),调用后进入 WAITING,等**LockSupport.unpark(thread)**唤醒
TIMED_WAITING:时间到了会自动唤醒,不用死等信号
带超时的等待,比如 sleep(1000)或 wait(1000);
触发场景
Thread.sleep(long):不释放任何锁,超时后自动唤醒(进入 RUNNABLE)
object.wait(long):释放 synchronized 锁,超时或被 notify()唤醒;
thread.join(long):主线程等 t 执行完,最多等 long 毫秒;
LockSupport.parkNanos(long)/parkUntil(long):带超时的暂停,超时或被 unpark 唤醒
TERMINATED:run () 执行完 退场
sleep(1000)和 wait(1000)有什么区别?
sleep(1000)和 wait(1000)都进 TIMED_WAITING
但前者抱着锁睡,后者放了锁**睡 **
这也是为什么 sleep()不能用来做线程协作(会占着锁不让),而 wait()可以
线程状态转换流程是怎么样的?
从 WAITING 被 notify()唤醒后,不会直接回 RUNNABLE,而是先去抢锁
抢不到就进 BLOCKED,抢到了才回 RUNNABLE;
举例:线程 A 持有锁并调用 wait()释放锁,线程 B 拿到锁后 notify()A,A 被唤醒后会先尝试重新拿锁,拿不到就堵在 BLOCKED
任务载体
Thread 类和 Runnable 有哪些区别?为什么推荐使用 Runnable?
Thread 是 线程本体,自带启动(start ())、中断等能力,我们可以通过 extends Thread 来实现一个线程。
但是如果这个类本身已经继承了一个父类呢?由于 Java 只有一个父类,所以这种场景是不是有点别扭?但是可以
implements 多个接口,所以推荐使用 Runnable 接口;
Runnable 代码示例
Callable 与 Runnable 有什么区别?
Callable 有返回值、支持受检异常;Runnable 无返回值、仅支持非受检异常
Callable 代码示例
FutureTask 有啥作用?
它能把 Callable/Runnable 包起来:
get():阻塞等结果
cancel():中途取消任务
状态判断:isDone()看任务是否完成
FutureTask 代码示例
线程协作
多线程配合就像打配合战,没默契就会乱套,这些机制就是 战术暗号
为什么 wait ()/notify () 必须在锁内调用?
底层原因:防止 信号丢失。
比如线程 A 想等信号,还没进 wait (),线程 B 就 notify () 了,A 再进 wait () 就永远等不到(信号过期)
加锁后,A 的 等信号 和 B 的 发信号 被原子化,避免这种情况
必须先拿锁(synchronized 块),否则抛 IllegalMonitorStateException
join () 让主线程 等待 的原理
主线程调用 t.join(),其实是主线程进入 t 对象的 wait () 状态(释放 t 的锁)
等 t 执行完,JVM 会自动调用 t.notifyAll()唤醒主线程。
案例:主线程要汇总 3 个子线程的计算结果,用 join () 确保子线程都跑完再汇总,否则可能拿到空数据。
volatile 与 wait/notify 的区别有哪些?
volatile 只能 传状态(比如开关),不能让线程挂起;
wait/notify 能 精准控节奏(比如 “生产完再消费”),但必须带锁。
所以 别用 volatile 做计数,比如 count++,因为它不保证原子性
线程安全与切换
线程不安全是指什么意思?
指多线程抢共享变量时,结果和预期不符。
本质:共享可变状态 + 非原子操作
什么是线程的上下文?
本质是线程执行到某一时刻的 全部状态信息
就像你写报告写到一半去接电话,回来时需要知道** 刚才写到第 3 段第 2 行**,光标在哪个字后面,脑子里刚想到的案例是什么
这些 状态 就是你继续写报告的 上下文
上下文具体包含这些关键信息:
CPU 寄存器 : 比如线程 A 正在算 1+2+3,寄存器里可能存着 3(1+2 的结果),这是它继续算下一步 +3 的基础 ,就像 你脑子里 临时记住的中间结果
程序计数器(PC):记录线程下一条要执行的指令地址,就像 你夹在报告里的 书签
栈指针:指向线程栈的顶部(Java 线程栈存局部变量、方法调用链路等),你记在草稿纸上的任务清单和层级
内存页表:线程访问内存时的地址映射关系(虚拟地址到物理地址的转换),确保线程能正确读写自己的内存数据,你电脑里的 文件路径索引(当前需要写的文件和资料精确的位置,记不住的话就得全局翻查资料,甚至找不到丢失了思路)
什么是 上下文切换?为什么会有上下文切换?
上下文切换,就是 CPU 从执行线程 A 切换到执行线程 B 时,必须做的 保存 A 的状态 + 加载 B 的状态 的过程
会有上下文切换的原因:
CPU 核心数量有限,但需要执行的线程 / 任务数量远多于核心数。
CPU 必须通过 轮流执行 让多个线程 看似同时运行,而切换就是 **轮流 **的实现方式
切换太频繁会有什么后果?如何减少切换?
时间浪费:保存 / 恢复现场占 CPU 时间(比如一次切换要 1us,100 万次就是 0.1 秒)
缓存失效:线程 A 的缓存数据对线程 B 没用,CPU 得重新从主存读,变慢。
减少切换方案:
线程池核心数:CPU 密集型(比如计算)设成 核心数 ±1,IO 密集型(比如数据库操作)设成 核心数 * 2
无锁编程:用 CAS(比如 AtomicInteger)代替 synchronized,线程不用阻塞等锁,减少切换
虚拟线程:IO 阻塞时自动 “让出” 内核线程,避免内核级切换。
总结
今天把并发基础这块地基打牢了!
线程状态咋转换、Thread 和 Runnable 的区别、wait/notify 这些通信套路,还有线程不安全的本质、上下文切换的开销,全捋得明明白白~
下一场该练并发里的 关键字王牌了!volatile 咋保证可见性、final 在并发里的特殊作用、CAS 的底层逻辑、Unsafe 的硬核操作,还有 ThreadLocal 的线程隔离套路。。。 这些可是并发安全的核心考点,必须吃透原理,练出实战手感。
熟练度刷不停,知识点吃透稳,下期接着练~
版权声明: 本文为 InfoQ 作者【DonaldCen】的原创文章。
原文链接:【http://xie.infoq.cn/article/f951c31ddaaadf86d67dc253e】。文章转载请联系作者。







评论