JUC并发编程知识一(待完善)
进程与线程
1.什么是进程?
进程是系统运行程序的基本单位,一个程序的运行就是一个进程。如:点开微信.exe文件、浏览器.exe文件
- 程序是静态:程序是静态的指令集合,是存储在磁盘上的可执行文件。如硬盘上的 “微信.exe”“浏览器.exe”
- 进程是动态:进程是程序的动态执行过程,当程序被操作系统加载到内存中,系统为其分配资源
2.什么是线程?
线程是 CPU 调度的最小单位,一个进程可以有多个线程,多个线程共享进程的堆和方法区资源,每个线程都有自己程序计数器、本地方法栈、虚拟机栈。
3.Java 程序启动时的默认线程数
Java 程序启动时最核心的两个线程:main 线程(用户线程),GC 线程(守护线程)
最少情况下:大约 4~5 个线程数(main + GC + Finalizer + Reference Handler + Signal Dispatcher)
| 对比项 | 用户线程(普通线程) | 守护线程(Daemon) |
|---|---|---|
| 生命周期 | 主线程结束后仍可运行 | 只要所有用户线程结束,它自动退出 |
| 应用场景 | 执行业务逻辑 | 做后台任务,如日志、监控、GC等 |
| 设置方式 | 默认是用户线程 | 手动调用setDaemon(true)设置 |
| JVM 退出行为 | 至少一个用户线程在,JVM 不退出 | 全部用户线程结束,JVM 自动退出 |
4.CPU和线程的关系
线程:线程是程序中的一段逻辑(如: for (int i = 0; i < 3; i++) ),逻辑不能够自己进行运行,需要通过CPU来执行。
CPU:cpu是执行这线程的硬件,cpu主要功能就是从内存中读取指令、解码指令、执行指令
需要注意的是:一个cpu只能执行一个线程任务
2.上下文切换
1.什么是上下切换?
上下文切换是指CPU从一个线程切换到另一个线程时,保存当前线程的执行状态并加载下一个线程的执行状态,当前线程通过记录的执行状态恢复执行。
2. “上下文” 是什么?
上下文是线程能够正常运行的核心条件之一,上下文包含了线程能够运行的所有状态数据,包含有程序计数器、本地方法栈、虚拟机栈
3.上下文切换的一些原因
- 时间片用尽,当线程的时间片用尽时,操作系统会将其状态保存,切换到另一个线程。
- 调用了sleep、wait,线程进入等待状态,线程让出cpu
- 线程执行完成
4. 上下文存储了哪些数据
- 程序计数器:记录当前线程下一条要执行的指令地址,确保线程切换后能从暂停处继续执行。
- 虚拟机栈:存储方法调用的栈帧,包含局部变量表、操作数栈、方法返回地址等。
- 本地方法栈:用于支持 Native 方法执行的栈结构。
- 寄存器状态:CPU 寄存器中与当前线程相关的数据(如通用寄存器、状态寄存器等),记录运算中间结果等临时数据。
- 线程私有数据:如线程 ID、优先级、状态标记(新建 / 运行 / 阻塞等)。
- 锁相关信息:线程持有的锁、等待的锁队列等(尤其在并发场景中)。
3.如何创建线程
1. 前置知识
①匿名内部类
匿名内部类是没有名字的局部类,它是在定义类的同时创建对象,常用来快速实现接口或抽象类的一个实例
语法格式:接口/父类 类型 变量名 = new 接口/父类() { // 实现或重写方法 };public static void main(String[] args) { Runnable r = new Runnable() { @Override public void run() { System.out.println("Hello Lambda!"); } }; r.run(); }传统写法 public class MyRunnable implements Runnable { @Override public void run() { System.out.println("Hello Lambda!"); } } public class Main { public static void main(String[] args) { Runnable r = new MyRunnable(); r.run(); // 或者 new Thread(r).start(); } }②Lambda 表达式
用于替代匿名内部类。它让我们写代码时更简洁、更函数式。
语法格式:(参数列表) -> { 代码块 }
简化:参数 -> 表达式
public static void main(String[] args) { Runnable r = () -> System.out.println("Hello Lambda!"); r.run(); }③函数式接口
函数式接口是只包含一个抽象方法的接口。它可以被 Lambda 表达式、方法引用等简洁语法所实现。
@FunctionalInterface是可选的注解,用于明确声明该接口是函数式接口,如果你写了多个抽象方法,编译器会报错。
| 接口名 | 抽象方法 | 说明 |
|---|---|---|
| Function<T,R> | R apply(T t) | 输入 T,返回 R |
| Consumer | void accept(T t) | 接收 T,无返回 |
| Supplier | T get() | 无参数,返回 T |
| Predicate | boolean test(T t) | 输入 T,返回布尔值 |
| Runnable | void run() | 无参无返回(线程) |
| Callable | V call() | 可抛异常,有返回值 |
官方定义(来自 Java 8) @FunctionalInterface public interface MyFunction { void apply(); }示例:
@FunctionalInterface interface MyPrinter { void print(String msg); }使用 lambda 表达式实现它 public static void main(String[] args) { MyPrinter p = (msg) -> System.out.println("打印: " + msg); p.print("Hello!"); }④方法引用
它是Lambda 表达式的简化写法
语法格式:类名/对象名::方法名
| 类型 | 示例 | 等价 Lambda 表达式 | 说明 |
|---|---|---|---|
| 1. 引用静态方法 | ClassName::staticMethod | x -> ClassName.staticMethod(x) | 适用于静态方法 |
| 2. 引用实例方法(特定对象) | instance::method | x -> instance.method(x) | 对象已存在 |
| 3. 引用实例方法(任意对象) | ClassName::method | (obj, x) -> obj.method(x) | 用于 Stream 等场景 |
| 4. 引用构造方法 | ClassName::new | () -> new ClassName() | 用于构造对象 |
⑤stream流
Java 8 引入的一种用于处理集合数据的抽象概念,它代表一系列元素的管道式计算,支持链式调用、函数式编程、并行处理等操作
| 步骤 | 描述 | 举例 |
|---|---|---|
| 1. 获取流 | 从集合或数组等生成流 | list.stream() |
| 2. 中间操作 | 处理数据(可多个) | filter()、map()、sorted() |
| 3. 终止操作 | 触发执行 | forEach()、collect()、count() |
示例:
import java.util.*; import java.util.stream.*; 找出长度大于 3 的字符串并转换为大写 public class Main { public static void main(String[] args) { List<String> fruits = Arrays.asList("apple", "kiwi", "banana", "fig", "grape"); fruits.stream() // 1. 获取流 .filter(f -> f.length() > 3) // 2. 中间操作:过滤 .map(String::toUpperCase) // 2. 中间操作:转换大写 .forEach(System.out::println); // 3. 终止操作:输出 } } 传统方式 import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class Main { public static void main(String[] args) { List<String> list = Arrays.asList("apple", "kiwi", "banana", "fig"); List<String> result = new ArrayList<>(); for (String item : list) { if (item.length() > 3) { result.add(item.toUpperCase()); } } for (String s : result) { System.out.println(s); } } }2. 线程创建方式
创建线程的方式有很多,但这些方式并不是真正的创建出线程,严格来说Java只有一种创建线程的方式,那就是“new Thread.start ()”,不管是哪种方式,最终还是依赖于“new Thread.start ()”
1. 继承 Thread 类,重写run()
public class ThreadExample extends Thread { @Override public void run() { System.out.println("子线程开始执行,"+ "线程名:" + Thread.currentThread().getName()); } } class ThreadTest{ public static void main(String[] args) { ThreadExample threadExample = new ThreadExample(); Thread thread = new Thread(threadExample); System.out.println("主线程开始执行," + "线程名:" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } 启动新线程,由新线程执行run()方法 threadExample.start(); } }2. 实现 Runnable 接口,重写run()
public class RunnableExample implements Runnable { @Override public void run() { System.out.println("子线程开始执行,"+ "线程名:" + Thread.currentThread().getName()); } } class ThreadTest{ public static void main(String[] args) { RunnableExample threadExample = new RunnableExample(); Thread thread = new Thread(threadExample); System.out.println("主线程开始执行,"+ "线程名:" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } 启动新线程,由新线程执行run()方法 thread.start(); } }3. 实现 Callable 接口 + Future ,重写call()
实现细节:重写call()方法,Callable对象封装到Future中,创建Thread对象,通过FutureTask的 get()方法获取返回值。
注意事项:在调用thread.start()之前就调用了futureTask.get(),会导致主线程一直阻塞,永远无法执行子线程。
public class CallableExample implements Callable { @Override public Object call() { System.out.println("子线程开始执行,"+"线程名:"+ Thread.currentThread().getName()); return 1; } } class CallableTest { public static void main(String[] args) throws ExecutionException, InterruptedException { CallableExample callableTest = new CallableExample(); FutureTask futureTask = new FutureTask<>(callableTest); Thread thread = new Thread(futureTask); System.out.println("主线程开始执行," + "线程名:"+ Thread.currentThread().getName()); Thread.sleep(3000); thread.start(); Object o = futureTask.get(); System.out.println("主线程获取返回值"+ o); } }4. 使用线程池创建
线程池创建线程的过程由其内部机制控制,核心是通过线程工厂生成新线程
3.Runnable和Callable的区别
1.是否有返回值
Runnable接口中的run()方法返回值类型为void,执行完成后没有返回值。Callable接口中的call()方法返回值类型为泛型,执行完成后有返回值。返回值通过Future获取。
2.是否能捕获异常
Runnable接口中run()方法抛出运行时异常,但无法捕获Callable接口中call()方法抛出异常通过Future捕获异常
4. 为什么Java除了最简单的继承Thread,还要设计Runnable、Callable+Future这些复杂的方式?
- Java里,一个类只能继承一个父类。如果你直接继承了
Thread,就没法继承别的类了,限制了代码的灵活性。 - 实现接口更灵活,可以多实现接口,还能继承别的类
- 通常完成业务需要返回值,通过
Callable方式能够获取返回值
4.线程生命周期
1. 线程生命周期有哪几种状态?
- 新建状态:指创建线程对象,还未调用 start(),线程还没开始运行。
- 就绪状态:指线程对象调用 start()方法进入就绪状态,等待CPU调度
- 阻塞状态:指代码加了同步锁,没有获取到锁的线程会被阻塞。(线程获取到锁后阻塞状态转换为就绪状态)
- 等待状态:指线程调用wait()、join(),线程需要等待被唤醒。(线程被唤醒,等待状态转换为就绪状态)
- 含时间的等待状态:指线程调用sleep( time ),线程会进入睡眠。(当超过睡眠时间,等待状态转换为就绪状态)
- 终止状态:指线程执行run()完成或者线程异常退出,然后线程生命周期结束。(当一个线程的状态变为终止状态后,无法通过任何方式让它再次执行,不能重新调用
start()方法让其再次运行)
2. 线程执行任务时什么情况下让出CPU?
- 线程执行run方法完成或线程异常退出时,线程会主动让出CPU
- 当线程进入阻塞状态或等待状态时,会被动让出 CPU
- 优先级较低的线程,会被动让出CPU
5.wait()、sleep()、join().......
1.wait()、sleep()、join()使用
wait()是 Java 中Object类的一个方法,让当前线程暂时放弃对象锁并进入阻塞状态,等待其他线程的唤醒后再继续执行。
- 必须在
synchronized同步代码块或方法中调用,且当前线程必须持有该对象的锁(否则会抛出IllegalMonitorStateException
Object lock = new Object(); // 线程A:进入等待状态 new Thread(() -> { synchronized (lock) { try { System.out.println("线程A:进入等待"); lock.wait(); // 释放锁,进入WAITING状态 System.out.println("线程A:被唤醒,继续执行"); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); // 线程B:唤醒等待的线程A new Thread(() -> { synchronized (lock) { System.out.println("线程B:准备唤醒"); lock.notify(); // 唤醒等待队列中的线程A System.out.println("线程B:唤醒完成"); } }).start();sleep()是Thread类的静态方法,用于让当前线程暂停执行,指定暂停时间(毫秒或纳秒),时间结束后线程自动唤醒并进入就绪状态,等待 CPU 调度。
new Thread(() -> { while (true) { System.out.println("定时任务执行中..."); try { Thread.sleep(3000); // 每3秒执行一次 } catch (InterruptedException e) { break; // 被中断时退出循环 } } }).start();join()是Thread类的方法,用于让当前线程等待目标线程执行完毕后再继续运行,核心作用是控制线程的执行顺序。
Thread t = new Thread(() -> { // 子线程任务 System.out.println("子线程执行中..."); try { Thread.sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } }); // 启动子线程 t.start(); // 主线程调用 t.join(),表示主线程等待 t 执行完毕 t.join(); System.out.println("主线程:子线程已完成,主线程开始执行"); }2.wait() 、sleep()、join()的区别
1.所属类不同
wait()是Object类的方法sleep()是Thread类的静态方法- join() 是
Thread类的方法
2.对锁的影响不同
wait():会释放当前持有的锁sleep():不会释放任何锁join():不会释放任何锁
3.唤醒机制不同
wait():必须由其他线程调用notify()或notifyAll()唤醒leep():睡眠时间到后自动唤醒join():一个线程完成任务后自动唤醒
3.notify()和notify() 的区别
notify():从等待队列中,随机叫醒一个在等待的线程notifyAll():叫醒所有在等待的线程
4.run()和start()的区别
1、调用次数限制不同
start():只能调用一次run():可以调用多次。
2、是否启动新线程
- 调用
start():会启动一个新线程,新线程执行run()中的逻辑 - 调用
run():不会启动新线程,而是在当前线程中直接执行run()中的逻辑
6. Java中线程之间如何进行通讯?
1. 线程之间为什么需要进行通讯?
开发一个电商系统,一个线程负责接收用户请求,一个线程负责库存数据更新,一个线程负责发送通知。如果各线程之间不进行相互合作,会导致电商系业务流程会混乱,数据不一致,最终用户流失。
“多个线程之间怎么‘沟通’ ”,比如怎么传递数据、怎么配合执行(谁先做谁后做)、怎么告知对方 “我做完了”“你可以开始了”。
2. 线程之间如何实现通讯
1、使用 wait()、notify()、notifyAll()方法
三种方法是Object类中定义的方法,必须与synchronized同步锁配合使用,wait()让线程进入等待状态,当满足条件时,通过notify()、notifyAll()唤醒线程,达到线程之间通讯
场景:适用于线程间的协调,如:生产者-消费模型,一个线程负责生产资源、一个线程负责消费资源
2、使用Condition对象
Condition对象有自己的wait()、notify()、notifyAll()方法
- await():让线程进入等待状态并释放锁
- signal():唤醒一个等待的线程
- signalAll():唤醒所有等待的线程
三种方法必须与lock锁配合使用,一个lock锁能够创建多个Condition对象等待队列,能够按条件进行分组管理线程。能够代替Object类中wait()、notify()、notifyAll()方法。
3、使用BlockignQueue对象
通过BlockignQueue对象提供put()向队列中添加元素,当队列满时会阻塞生产者;take()向队列中取出元素,当队列空时会阻塞消费者,该方式不需要手动处理线程等待和唤醒逻辑。
4、volatile关键字
当一个线程修改了用volatile修饰的变量,其他线程会立即看到该变量的状态变化,实现简单的信息传递。
7.线程优先级
优先级范围:线程优先级范围:1~10,默认线程优先级为5
设置与获取:
- 通过
setPriority(int newPriority)方法设置优先级; - 通过
getPriority()方法获取当前优先级。
===================================Thread源码======================================== /** * The minimum priority that a thread can have. * 线程可以拥有的最小优先级 */ public final static int MIN_PRIORITY = 1; /** * The default priority that is assigned to a thread. * 分配给线程的默认优先级 */ public final static int NORM_PRIORITY = 5; /** * The maximum priority that a thread can have. * 线程可以拥有的最大优先级 */ public final static int MAX_PRIORITY = 10;8.虚假唤醒
指:线程在没有被notify()或notifyAll()唤醒的情况下,自己跳过条件判断从wait()状态返回
1.为什么if会导致虚假唤醒问题?
wait()返回后,不会知道自己是“被真正唤醒”还是“被虚假唤醒”
if只判断一次条件;一旦
wait()返回(无论真假),if不会再检查条件;如果条件没满足就往下执行,就出问题了。
2. 正确方式(用while保证条件检查)
while (number !=0){ this.wait(); } number ++; System.out.println(Thread.currentThread().getName() + "," + number); this.notifyAll();9.死锁
1.死锁四大必要条件
- 互斥条件:一个资源只有一个线程持有
- 持有并等待:一个线程持有资源的同时,请求新的资源
- 不可剥夺:线程持有的资源不能被强制剥夺,除非自己释放资源
- 循环等待:每个线程都在等待下一个线程释放资源,形成循环
2.线程死锁
两个或两个以上的线程持有的资源都是对方所需要的,但都不释放资源,线程之间相互等待,这个现象就是死锁。
3.如何避免线程死锁
打破产生死锁的四大条件中的一个就可避免死锁
- 按顺序申请资源:每个线程按相同顺序获取锁,如先线程1获取lock1锁,其他线程必须等待lock1释放才能够获取lock2锁 (破坏 “循环等待”)
- 一次性获取所有资源:线程在执行前,先尝试获取所有需要的资源,要么全部获取,要么一个都不持有(破坏 “持有并等待”)
- 使用尝试锁机制:使用
ReentrantLock的tryLock(timeout)方法:尝试一段时间内获取锁,如果超时未获取到锁,则放弃获取或释放已持有的锁。 (破坏 “不可剥夺”) 减少锁的持有时间:线程持有锁的时间缩短,其他线程也能够在短时间内获取到锁
4.死锁检测方式
方式一:Java自带的 jps命令 + jstack命令
jps命令:查看 Java进程的ID
jstack命令:根据线程ID,排查线程死锁、阻塞、CPU 飙高、程序卡死等问题。
方式二:第三方工具
阿里巴巴的Arthas
