JUC并发编程(一)
目录
一 基础概念
1 进程与线程概念
2 并发和并行
3 同步和异步
4 杀死查看进程
5 栈与栈帧
6 线程的上下文切换
二 常见用法
1 创建线程的几种方式
(继承Thread类
(实现Runnable接口(推荐)
(实现Callable接口
2 开启线程 - start
3 暂停线程 - sleep / yield
4 线程的等待 - join
5 线程的中断 - interrupt
(线程处于Runnable状态
(线程处于Time_waiting
6 守护线程 - daemon
7 线程优先级 - Priority
8 线程的基本属性 id state name
9 park/unpark
三 两阶段终止模式
四 线程的状态
一 基础概念
1 进程与线程概念
1 进程(Process):一个正在运行的程序,操作系统分配资源的基本单位。拥有独立的内存空间。
2 线程(Thread):一个进程内部的一条独立执行的路径,CPU调度的基本单位,共享所处进程的内存空间和资源。
2 并发和并行
1 并发(Concurrency):同时处理多个任务的能力,在单个CPU核心,通过快速执行不同的任务,让用户感受到多个任务在“同时”执行(并非真正的同时)。
2 并行(Parallelism):同时执行多个任务的能力,在多个CPU核心或多个处理器,实现真正的任务同时执行。
3 同步和异步
同步:需要等待结果返回才可以继续运行。
异步:不需要等待结果返回就可以继续运行。
4 杀死查看进程
cmd命令行
- 查看所有进程 tasklist
- 查看指定格式的进程 tasklist | findstr <java>
- 查看所有的java进程 jsp
- 查看某个进程的所有线程状态 jstack <pid>
5 栈与栈帧
栈:JVM为每一个线程分配一个虚拟机栈,其生命周期与线程相同。
栈帧:栈由多个栈帧组成,每个栈帧对应一次方法调用。
栈帧的组成部分:
- 局部变量表
- 操作数栈
- 动态链接
- 返回地址
6 线程的上下文切换
当发生上下文切换(Context Switch)时,操作系统会负责保存当前线程的状态,并恢复目标线程的状态。在 Java 中,这一机制的核心体现是程序计数器,其作用是记录下一条 JVM 指令的执行地址。
程序计数器:程序计数器就是上下文切换时必须保存/恢复的关键状态,没有它线程就会失忆!就像一个书签。
一些情况:
- 线程的CPU时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法。
频繁切换会有性能问题,其需要保存与切换,要根据业务进行设计。
二 常见用法
1 创建线程的几种方式
(继承Thread类
线程与业务耦合,每个线程独立,并且不适配线程池。
Thread t1 = new Thread("t1"){@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println("Hello World!" + Thread.currentThread().getName());}}};t1.start();
(实现Runnable接口(推荐)
显式定义Runnable实例并共享,实现多个线程共享,并支持线程池
Runnable r1 = new Runnable() {@Overridepublic void run() {System.out.println("Hello World!" + Thread.currentThread().getName());}};Thread t3 = new Thread(r1);t3.setName("t3线程");
Runnable r = () -> {for (int i = 0; i < 100; i++) {System.out.println("Hello World!"+ Thread.currentThread().getName());}};Thread t1 = new Thread(r);t1.setName("t1线程");Thread t2 = new Thread(r);t2.setName("t2线程");t1.start();t2.start();
下面这种方式也是实现类Runnable接口,但是并不具备多个线程可以共享Runnable实例的优点,优点是代码简洁了,可以使用在业务不需要多个线程共享资源的情况,相同的逻辑但是会创建多个Runnable实例。
Thread t3 = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("Hello World!" + Thread.currentThread().getName());}});t3.setName("t3线程");t3.start();
Thread t1 = new Thread(() -> {for (int i = 0; i < 10; i++) {System.out.println("Hello World!" + Thread.currentThread().getName());}
});
t1.setName("t1线程");
t1.start();
(实现Callable接口
实现一个实例得到的结果可以给多个线程使用,避免了多个线程等待同一个耗时任务的结果,避免了重复计算,实现了”计算一次,多次使用“的模式.
组件 | 作用 |
---|---|
Callable<Integer> | 带返回值的任务接口(类似 Runnable ,但可返回结果和抛出异常) |
FutureTask<Integer> | RunnableFuture 的实现类,包装 Callable 任务,兼具 Runnable 和 Future 功能 |
Thread | 执行任务的线程载体 |
Callable定义的是call方法,有返回值。Runnable定义的是run方法,无返回值。
FutureTask起到了桥梁的作用,Future底层会实现RunnableFuture(这个接口继承了Runnable和Future接口,其有Runnable的run方法可以被线程直接执行,又具有Future的特性,可以获得异步计算的结果)
将实例的具体运行结果放在了重写的run方法当中
public class FutureTask<V> implements RunnableFuture<V> {public void run() {// 内部会调用 Callable.call()V result = callable.call();set(result); // 存储结果}
}
当多个线程共享同一个Futuretask实例的情况下,FutureTask的设计会确保任务只会执行一次。
1 FutureTask内部会维护一个volatile的状态变量state,初始状态为new
- new - > completing - > normal 正常完成
- new - > completing - > exceptional 异常完成
- new - > cancelled 被取消
- new - > interrupting - > interrupted 被中断
private static final int NEW = 0; // 初始状态
private static final int COMPLETING = 1; // 临时状态(结果设置中)
private static final int NORMAL = 2; // 正常完成
private static final int EXCEPTIONAL = 3; // 异常完成
private static final int CANCELLED = 4; // 已取消
private static final int INTERRUPTING = 5; // 中断中
private static final int INTERRUPTED = 6; // 已中断
2 run方法(里面调用Callable的call方法)
当一个线程调用run时,会先检查状态是不是new,如果不是方法返回什么也不做。
如果状态是new尝试将任务执行的线程设置为当前线程,保证原子性。
3 get方法被线程调用时,会先判断状态是否为最终的结果状态,normal/exceptional等,如果是直接返回,如果不是将进入阻塞队列,等待正在运行的线程执行完成,返回结果。
实现:
public static void main(String[] args) {Callable<Integer> taskLogic = () -> {String threadName = Thread.currentThread().getName();System.out.println(threadName + " 开始计算...");int sum = 0;for (int i = 0; i < 100; i++) {sum += i;}System.out.println(threadName + " 计算完成");return sum;};// 关键点:创建一个共享的FutureTask实例FutureTask<Integer> sharedTask = new FutureTask<>(taskLogic);// 线程1:启动任务执行Thread t1 = new Thread(sharedTask, "t1线程");t1.start();// 线程2:只获取结果(不重复执行任务)Thread t2 = new Thread(() -> {try {// 直接获取结果(会阻塞直到计算完成)Integer result = sharedTask.get();System.out.println(Thread.currentThread().getName() + " 获取结果: " + result);} catch (Exception e) {e.printStackTrace();}}, "t2线程");t2.start();// 主线程:获取结果try {Integer mainResult = sharedTask.get();System.out.println("主线程获取结果: " + mainResult);} catch (Exception e) {e.printStackTrace();}}
2 开启线程 - start
Thread t = new Thread(() -> {// 线程任务代码
});
t.start(); // 关键!启动新线程(调用run()不会启动新线程,在main方法当中调用只会在主线程当中执行)
3 暂停线程 - sleep / yield
礼让yield线程的核心目的是主动放弃当前线程的CPU执行权,让其他线程有机会执行。最终的执行权还是在任务调度器那里。
// 睡眠(单位:毫秒)
Thread.sleep(1000); // 当前线程睡1秒// 礼让(建议调度器让出CPU)
Thread.yield(); // 类似"你先用CPU,我歇会"
4 线程的等待 - join
Thread downloadThread = new Thread(下载任务);
downloadThread.start();// 主线程等待downloadThread完成
downloadThread.join(); // 带超时的等待(最多等3秒)
downloadThread.join(3000);
5 线程的中断 - interrupt
(线程处于Runnable状态
线程正常运行,但是需要加上中间标志位
这段代码的执行过程,首先主线程开启了一个新线程,新线程执行其中的run 方法,主线程同时睡眠1s睡眠结束,主线程将中间标志位设置为true,子线程将会在下一次的while循环跳出循环,但不会立刻终止线程,当线程真正执行到run方法的末尾才会真正结束线程。
Thread t = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("线程正在运行...");// 模拟耗时操作(无阻塞)}System.out.println("线程被中断,退出!");
});
t.start();// 主线程等待1秒后中断子线程
try {Thread.sleep(1000);
} catch (InterruptedException e) {e.printStackTrace();
}
t.interrupt(); // 设置中断标志位
(线程处于Time_waiting
线程使用sleep(time)、wait(time)、join(time)进入有效期等待状态
主线程启动子线程,子线程调用sleep5s,主线程调用sleep1s,睡眠结束调用interrupt,中断标志位被设置为true,但是由于线程处于阻塞状态会抛出异常,同时因为sleep的默认行为会将中断标志位清除变为false,需重新设置。(这是 sleep()
的默认行为,目的是让开发者有机会在异常处理后重新设置中断标志)
Thread t = new Thread(() -> {try {Thread.sleep(5000); // 进入 TIMED_WAITING 状态} catch (InterruptedException e) {System.out.println("线程在 sleep 时被中断,中断标志:" + Thread.currentThread().isInterrupted()); // 输出:falseThread.currentThread().interrupt(); // 重新设置中断标志}});t.start();// 主线程等待1秒后中断子线程try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}t.interrupt();
6 守护线程 - daemon
Thread daemon = new Thread(后台任务);
daemon.setDaemon(true); // 设置为守护线程
daemon.start(); // 主线程结束时自动终止
7 线程优先级 - Priority
守护线程是为用户线程提供服务的后台线程,JVM会在所有用户线程结束后自动终止守护线程。
Thread highPriority = new Thread(重要任务);
highPriority.setPriority(Thread.MAX_PRIORITY); // 最高优先级(1-10范围)Thread lowPriority = new Thread(次要任务);
lowPriority.setPriority(Thread.MIN_PRIORITY); // 最低优先级
8 线程的基本属性 id state name
Thread current = Thread.currentThread(); // 获取当前线程
System.out.println("线程ID: " + current.getId());
System.out.println("线程名: " + current.getName());
System.out.println("状态: " + current.getState()); // NEW/RUNNABLE/BLOCKED等
9 park/unpark
使用后线程进入waiting等待时间,可以使用两种方式来唤醒
特性 | LockSupport.unpark() | Thread.interrupt() |
---|---|---|
中断状态 | 不改变中断状态 | 设置中断状态为true |
唤醒时机 | 可先于park调用 | 必须在park之后调用 |
精确控制 | 可指定唤醒线程 | 只能中断当前线程 |
阻塞响应 | 直接唤醒 | 唤醒并设置中断标志 |
使用场景 | 正常协作 | 异常中断处理 |
代码实现:
import java.util.concurrent.locks.LockSupport;import static java.lang.Thread.sleep;public class Thread1 {public static void main(String[] args) throws InterruptedException {test1();}public static void test1() throws InterruptedException {Thread park = new Thread(() -> {System.out.println("park");LockSupport.park();System.out.println("unpark" + Thread.currentThread().isInterrupted());});park.start();sleep(1000);LockSupport .unpark(park);//借助LockSupport实现线程中断
// park.interrupt();//借助Thread.interrupt()实现线程中断}
}
三 两阶段终止模式
两阶段终止模式(Two-Phase Termination Pattern)是一种多线程编程中安全终止线程的设计模式。它的核心思想是:在终止线程前,先发出终止请求(第一阶段),然后线程在完成必要清理工作后再真正终止(第二阶段)。这种模式避免了直接强制终止线程导致的资源泄露、数据不一致等问题
简单来说就是在一个线程T1中如何优雅的终止线程T2,给T2一个料理后事的机会
在某些线程正在运行时如何正确终止线程,如果强制stop会带来相对应的问题:
- 1 资源未释放:线程可能持有锁或者打开文件或者网络连接未关闭
- 2 数据不一致:线程可能正在修改共享数据,突然终止会导致数据损坏。
- 3 不可控性:无法保证线程在安全点终止。
需要借助interrupt进行打断,存在一个中断标志位,可以在这个中断循环之外将其他需要善后的地方再进行操作,可实现优雅终止。在下面代码实现的过程当中需要注意是在睡眠当中还是在运行当中被打断。
package day01;public class ax5 {public static void main(String[] args) {TwoPhaseTermination tpt = new TwoPhaseTermination();tpt.start();try {Thread.sleep(3500);} catch (InterruptedException e) {System.out.println("main thread stop");}tpt.stop();}
}class TwoPhaseTermination {private Thread monitor;public void start() {monitor = new Thread(() -> {while (true) {Thread current = Thread.currentThread();if (current.isInterrupted()) {System.out.println("打断线程,料理后事");break;} else {try {Thread.sleep(1000);System.out.println("执行监控记录");} catch (InterruptedException e) {current.interrupt();// 重置标志位位}}}});monitor.start();}public void stop() {monitor.interrupt();}
}
补充:
interrupted() | 静态方法 | 当前线程(调用 Thread.interrupted() ) |
isInterrupted() | 实例方法 | 指定线程对象(如 thread.isInterrupted() ) |
interrupted() | - 当前线程需要检查中断状态并主动清除标志(如处理完中断后)。 |
- 需要确保中断状态不会被后续逻辑误判(如循环中检查中断状态)。 | |
isInterrupted() | - 检查其他线程的中断状态(如监控子线程是否被中断)。 |
- 需要多次检查中断状态且保留标志位(如在循环中持续监控)。 |
四 线程的状态
线程6种状态_线程状态-CSDN博客
五种状态
Question:sleep方法会让线程睡眠,线程睡眠时间到了之后,会立马执行下面的代码吗?
- 当睡眠时间到期后,线程会从阻塞状态变为可运行状态(Runnable),但并不保证它会立即执行。
- 即使睡眠时间到期,线程也需要与其他可运行状态的线程竞争CPU资源。
- 因此,睡眠时间到期后,线程可能会立即执行,也可能需要等待一段时间才能得到CPU时间片。
- 操作系统的线程调度器决定哪个线程获得CPU时间片。