大家好,我是程序员强子。
又来刷英雄熟练度咯~今天专攻 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代码示例
class FileRunnable implements Runnable {
@Override
public void run() {
// 尝试读文件(可能抛IOException,属于受检异常)
try {
Files.readAllBytes(Paths.get("test.txt"));
} catch (IOException e) {
// 只能捕获,无法向上抛(run()声明不允许抛受检异常)
e.printStackTrace();
}
// 无返回值,若想把读取结果给主线程,只能用共享变量(麻烦)
}
}
Callable 与 Runnable有什么区别?
Callable 有返回值、支持受检异常;Runnable 无返回值、仅支持非受检异常
Callable 代码示例
// 泛型参数<String>表示返回值类型
class FileCallable implements Callable<String> {
private String filePath;
public FileCallable(String filePath) {
this.filePath = filePath;
}
// 有返回值,且声明抛出受检异常
@Override
public String call() throws Exception {
// 读文件,直接抛出异常(由调用方处理)
byte[] data = Files.readAllBytes(Paths.get(filePath));
return "文件内容:" + new String(data); // 返回结果
}
}
FutureTask有啥作用?
它能把 Callable/Runnable 包起来:
- get():阻塞等结果
- cancel():中途取消任务
- 状态判断:isDone()看任务是否完成
FutureTask 代码示例
public class FutureTaskDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1. 创建Callable任务
FileCallable callable = new FileCallable("test.txt");
// 2. 用FutureTask包装Callable(FutureTask实现了Runnable)
FutureTask<String> futureTask = new FutureTask<>(callable);
// 3. 把FutureTask传给Thread(因为它是Runnable)
Thread thread = new Thread(futureTask);
thread.start();
// 4. 主线程通过futureTask获取结果(会阻塞,直到任务完成)
String result = futureTask.get();
System.out.println("任务结果:" + result);
// 其他常用方法
System.out.println("任务是否完成:" + futureTask.isDone()); // true
System.out.println("任务是否被取消:" + futureTask.isCancelled()); // false
}
}
线程协作
多线程配合就像打配合战,没默契就会乱套,这些机制就是 战术暗号
为什么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 的线程隔离套路。。。 这些可是并发安全的核心考点,必须吃透原理,练出实战手感。
熟练度刷不停,知识点吃透稳,下期接着练~