JUC 概述
并发,并行,串行
- 并发:宏观上同时进行,微观上交替切换执行,单个 CPU 核心,靠快速切换任务
- 并行:同一时刻,多个任务真正同时运行,多个 CPU 核心,每个核心独立跑一个任务
- 串行:同一时刻只做一件事,做完一件再做下一件
线程
什么是线程
一个程序运行时,内部可以分成一个或多个线程,线程是CPU 执行任务的最小单位
创建线程的方式
实现 Runnable
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable 线程:" + Thread.currentThread().getName());
}
}
public class Test {
public static void main(String[] args) {
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.start();
}
}
继承 Thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程运行:" + Thread.currentThread().getName());
}
}
public class Test {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start(); // 启动线程
}
}
实现 Callable
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("Callable 线程运行");
return 100; // 可以返回结果
}
}
public class Test {
public static void main(String[] args) throws Exception {
FutureTask<Integer> task = new FutureTask<>(new MyCallable());
Thread t = new Thread(task);
t.start();
// 获取返回值
Integer res = task.get();
System.out.println("结果:" + res);
}
}
线程池创建
public class Test {
public static void main(String[] args) {
// 创建线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
// 提交任务
pool.submit(() -> {
System.out.println("线程池线程:" + Thread.currentThread().getName());
});
pool.shutdown(); // 关闭线程池
}
}
线程的生命周期
- 新建,创建了一个线程对象,但是没有调用 start() 方法
- 就绪,调用了 start(),等着 CPU 分配时间片,一旦拿到 CPU,就开始执行 run()
- 阻塞,线程阻塞于锁
- 等待,主动无限期等别人唤醒,例如:wait() / join() / LockSupport.park()
- 定时等待,有时间限制的等待,例如:sleep(1000) / wait(1000) / join(1000)
- 终止,run() 执行完 / 异常退出
状态的流转关系
NEW → start() → RUNNABLE
RUNNABLE
↓ 抢锁失败
BLOCKED → 拿到锁 → RUNNABLE
RUNNABLE
↓ wait()/join()
WAITING → notify()/notifyAll() → RUNNABLE
RUNNABLE
↓ sleep(1000)
TIMED_WAITING → 时间到 → RUNNABLE
RUNNABLE → 执行完毕 → TERMINATED
如何终止线程
stop
强制杀死线程,已弃用
interrupt
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("运行中...");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
// 收到中断信号,退出
System.out.println("线程被中断终止");
}
});
t.start();
Thread.sleep(2000);
t.interrupt(); // 中断
}
}
interrupt()、interrupted()、isInterrupted()的作用以及区别
-
thread.interrupt()
作用:中断线程(设置 interupted = true)
类型:实例方法
效果:
如果线程正在运行:仅设置中断标志,不停止运行
如果线程在 sleep()/wait()/join():会抛出 InterruptedException,并清除中断标志
不会强制停止线程,只是通知 -
thread.isInterrupted()
作用:查询是否被中断
类型:实例方法
特点:
只读取,不改变状态
调用一次,标记依然保留 -
Thread.interrupted()
作用:查询 + 清除中断标记
类型:静态方法
特点:
第一次调用返回 true
第二次调用一定返回 false(因为复位了)只针对当前执行的线程,例如在 main 线程中执行 t1.interrupted(),其实查询的是 main 线程的 interupted 变量并复位
synchronized
锁升级
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
单向升级,不可降级,只有释放锁后才会变回无锁
无锁
默认的无锁状态
偏向锁(jdk 15+ 默认取消偏向锁)
锁一直被同一个线程拿
第一次抢锁:CAS 把当前线程 ID 写入对象头,之后再来,只要线程 ID 匹配,直接进,不需要 CAS
第二个线程来竞争时,偏向锁被撤销 → 升级轻量级锁
轻量级锁
两个线程交替用锁,无长时间阻塞
线程在自己的栈帧创建 Lock Record,把对象头复制到 Lock Record 中,通过 CAS 把对象头换成指向自己 Lock Record 的指针
成功:拿到锁
失败:将锁升级为重量级锁,然后进入 cxq 链表进行自旋,自旋成功则获取锁,自旋多次失败则会进入阻塞状态,等待唤醒
重量级锁
monitor-enter
每一个对象都会和一个监视器monitor关联, 监视器被占用时会被锁住, 其他线程无法来获取该监视器, 当JVM执行某个对象的 monitorenter(synchronized(obj)) 方法时, 会尝试去获取对象的监视器所有权, 过程如下:
- 如果monitor 的进入数为 0, 则线程可以进入monitor, 并将 monitor 的进入数变为 1, 当前线程成为 monitor 的所有者
- 如果线程已经拥有了 monitor 的所有权, 允许重入 monitor, 此时 monitor 的进入数 +1
- 如果其他线程已经占有了 monitor, 当前线程尝试获取 monitor 所有权时会被阻塞, 直到 monitor 的进入数为 0
monitor-exit
- 能执行monitorexit指令的线程一定是拥有monitor所有权的线程
- 执行monitorexit指令后, monitor 的进入数 -1, 当进入数变为 0 后, 表示当前线程释放锁, 不再拥有 monitor 的所有权, 此时其他阻塞的线程可以去尝试获取monitor的所有权
synchronized 执行时, 没有竞争到锁的线程会被挂起, 此时需要调用操作系统的park() 方法, 竞争到锁的线程会被 unpark() 唤醒
并发带来的问题
原子性
一个或多个操作,要么全部执行成功,要么全部失败,中间不能被其他线程打断
问题原因:CPU 分时切换线程,多条指令被拆分穿插执行
例如:i++
解决方案:synchronized、Lock、Atomic 原子类
可见性
一个线程修改共享变量,其他线程能立刻感知到最新值
问题原因:线程会把主内存变量拷贝到本地缓存,修改后不会立即刷回主存,其他线程读到旧数据
解决方案:volatile、synchronized
有序性
代码顺序 = CPU 实际执行顺序
问题原因:指令重排序(编译器、CPU 为优化性能打乱指令)
重排序在单线程无影响,但多线程下破坏逻辑
解决方案:volatile、synchronized
MESI 协议
MESI 是多核 CPU 间维护缓存一致性的核心协议,配合总线嗅探与状态机转换,让同一份内存在多个核的缓存副本始终一致
- M(Modified,修改 / 脏)
当前核独有,数据已修改、与主存不一致(脏)
其他核无此缓存副本
后续必须写回主存 - E(Exclusive,独占 / 干净)
当前核独有,数据与主存一致(干净)
其他核无此缓存副本
写入时可直接转为 M,无需总线交互 - S(Shared,共享 / 干净)
多个核都有副本,数据与主存一致(干净)
读可共享,写必须先通知其他核失效 - I(Invalid,无效)
缓存行无效,数据不可用
相当于 “没缓存这行”
MESI 带来的问题
如果某个核写了数据,其他核都要阻塞等这个数据写入后,同步到自己的缓存再进行其他操作,效率太低,所以引入了 Store Buffer 和 Invalidate Queue 去异步写入数据或者无效化数据
写缓冲区(Store Buffer):异步化更新 CPU 缓存与内存数据,CPU 不等确认继续执行,可能导致内存重排
无效化队列(Invalidate Queue):延迟处理失效请求,提升吞吐,同样可能重排
解决方案
-
写屏障(Store Memory Barrier):清空当前CPU 的 Store Buffer,写屏障前所有写操作必须全部同步到其他Cache后,才能继续执行写屏障后的其他写操作;解决Store Buffer写重排
-
读屏障(Load MemoryBarrier):读取内存前,清空本地Invalidate Queue,所有失效消息执行完毕再读Cache;解决失效队列脏读
volatile 的原理
volatile的实现原理是内存屏障
-
对volatile修饰的变量进行写指令, 会加上写屏障,写屏障可以保证写屏障之前的代码都在写屏障之前执行而不会在写屏障后面执行, 从而保证不会被指令优化影响
-
对volatile修饰的变量进行读指令, 会加上读屏障,读屏障可以保证在读屏障之后的代码不会被指令优化到读屏障前面执行, 即对num的读取不可能发生在对ready的读取前
happens-before
定义
如果操作 A happens-before 操作 B,那么
- A 的执行结果 对 B 可见
- A 与 B 不会被指令重排
规则
程序顺序规则
单线程内,书写在前的操作 happens-before 书写在后的操作(无依赖的操作允许局部重排)
a = 1;
b = 2;
// 编译器/CPU 可以重排,但单线程结果不变,依然符合程序顺序规则
volatile 变量规则
对一个 volatile 变量的写操作,happens-before 后续所有线程对该变量的读操作
- 底层:volatile 读写前后插入内存屏障,禁止指令重排
- 传递性:volatile 写之前的所有普通变量,对后续 volatile 读的线程全部可见
锁规则
解锁操作 happens-before 后续同一把锁的加锁操作
- 线程 A 释放锁 → 线程 B 获取同一把锁:A 所有修改对 B 可见
- 等价于:锁天然保证可见性 + 原子性 + 有序性
线程启动规则
Thread.start() 方法 happens-before 该线程内的所有操作
主线程调用 start() 前的代码,对子线程完全可见
线程终止规则
线程内任意操作 happens-before 其他线程检测到该线程终止(Thread.join())
子线程所有修改,主线程 join() 后一定可见
线程中断规则
对线程调用 interrupt() happens-before 被中断线程检测到中断
传递性规则
如果 A happens-before B,且 B happens-before C
那么 A happens-before C
对象终结规则
对象构造方法执行完成 happens-before finalize() 方法开始执行
更多推荐




所有评论(0)