java 并发编程八股-多线程篇
概括,总结,反思,对比
java 操作的线程具体是什么
在不考虑 java 最新特性,虚拟线程的情况下,指代的就是系统线程,因为在虚拟线程这个概念出来的之前,java 就要一直有一个问题(用户态和系统态线程资源切换的一个性能问题。)。所以 java 操作的就是普通的系统线程。
但是在虚拟线程出来之后,就不是这样了,java 在真实系统线程和用户之间添加了一个中间层,让用户不在直接关注系统线程。这样就减少了线程操作的性能问题,更加轻量。
使用多线程要注意的问题
首先关注的就是三要素,原子性 ,可见性,有序性,。这三点就是使用多线程要注意的问题。防止多个线程争抢出现数据不一致的问题。
- 原子性
这里可以指定,变量操作的原子性,代码块操作的原子性。比如常见的超卖问题,原子递增,原子类等,都是要在多线程环境下让系统的执行结果符合你思考的结果。
-
可见性
可见性就要求变量的变更是及时的,当前线程更新完要让其他线程感知到更新。(上锁,解锁。volatile)
-
有序性
指令的执行结果,顺序也要符合预期。(要考虑到,指令重拍,volatile 特性)
创建线程的几种方法
- 继承 thread类
这样是最经典的方法,直接继承thread 类,new 对象。start()这种方式就是简单,缺定就是占用继承位置(java 不能多继承)
- 实现 Runnable 接口
这个也挺经典的,就是实现这个接口以及重写一个 run 方法。
在使用的时候是,new thread 对象并传递你的接口对象就行。(设计模式:装饰器-----唉,这个不对是,策略模式)
- 装饰器
装饰器,意在传递一个类型,额外增加功能(文件流 -》 文件缓冲流)
- 策略模式
这种模式的重点是,自己传递不同的算法实现,来达到预定的效果(你实现接口,重写的那个 run 方法)
IO 一类才是装饰器
- 使用线程池
这种方法是现在最常见的一种。
优点 就是,创建使用简单,线程也不会被回收,能够最大程度的自定义。
避免频繁的线程销毁与创建,实现线程的复用,降低性能的损耗。
**缺定:**恰恰就是自由度高,可复用性这两个缺定,所延伸的问题就是如何填写,合适的参数。
threadLocal 如和避免内存泄露(垃圾没有被回收,一直占用内存到满)。
解决方法:自定义实现(封装轮子,自己实现销毁移除操作。)
线程池
创建线程池的几种方法,为什么推荐使用。
来自 LLM:
推荐使用 ThreadPoolExecutor的原因:1.
资源可控 - 避免无界队列导致的内存溢出2.
行为明确 - 所有参数都可配置,行为可预测3.
稳定性强 - 适合生产环境的高并发场景4.
可监控性 - 提供丰富的状态监控方法5.
灵活性高 - 支持自定义线程工厂和拒绝策略虽然在开发测试阶段 Executors很方便,但在生产环境中必须使用 ThreadPoolExecutor来确保系统的稳定性和可靠性。
这里简单总结一下,后面有线程池篇章,再说。使用ThreadPoolExecutor的原因很多,但是也很简单。就是其他方式虽然简单,但是可控性,自定义性不高。还容易出现各种问题。(什么,无界队列,无限创建线程等问题)
如何停止线程
- 优雅停止
简单来说就添加标记信息,通过(线程调用interrupt())这个方法来给线程打标记,来检测这个标记位置并手动,抛出异常实现优雅停止。
在沉睡中死去:这种方法看着挺暴力,其实也是添加标记。先睡眠,在打标记,这里自动抛出中断异常并停机。
使用 return:在 run 方法中判断标记信息然后停机。
- 暴力停止
使用 stop 方法暴力停止。这个方法已经废弃,因为强制停机不知道会有什么问题。
线程的生命周期
初始状态, 线程对象创建了,但是还没有调用 start ()方法。
可运行状态,调用 start()方法后进入就绪状态。并等待 CPU 调用。
阻塞状态, 试图获取锁,等待锁释放。
等待状态,
含有等待时间的等待状态,
终止状态,执行结束或者因为异常终止。
sleap和wait 的区别
- sleap
这个方法属于 thread 类的静态方法。可以在任意位置调用。
不会放弃当前持有的锁。但是在这期间会释放 CPU 的时间片。
超过时间(休眠结束)会自动恢复。
- wait
这个方法属于实例,只能在同步代码块中调用。且会放弃当前持有的锁。
会让当前线程进入等待状态,暂停执行。
只能通过(notify/notifyAll)方法唤醒。
notify和notifyAll的区别
这两个都是用来唤醒被 wait 暂停的线程。宏观的区别是,notify 是唤醒一个,all 是唤醒所有。
微观的区别要看,jvm 的具体实现。有的就是随机唤醒。有点 jvm 会维护一个队列,notify 就是唤醒第一个。
不同线程之间是如何进行通信的
通信方式有很多:基本都是在维护一个先后的状态,不能同时进行。维护线程并发安全
- volatile保证可见性
这个标识符是用来标记变量的,让变量作为全局共享变量,让多个线程之间增加可见性。就是变更之后其他线程能够立刻感知。用这样的方式来编排线程。比如:通过 volatile 标识的变量 0 代表一种状态,true 又代表一种……
volatile极限状态下有什么问题。
在内存当中,和volatile同行的内存会被频繁刷新,让一部分内存和他自己无法使用到 cpu 缓存的优势,降低访问操作的效率。另一方面,因为变量的在频繁改,频繁刷新,其他线程都针对同一块内存的频繁访问。增加一个总线的压力,导致总线风暴。降低可用性。
总结:无法充分利用,jvm 重排序(并不都是坏的,因为大部分情况下用不到 volatile),cpu 缓存。的优势。
- synchronized,wait,notify
因为 wait,notify 只能用在实例方法中,同步代码块中。所有也要算上 synchronized。
简单举例来说,就是一个消息队列循环消费的场景(循环打印奇偶数)。
public class OddEvenPrinter {private final Object lock = new Object();private int number = 1;private final int maxNumber;public OddEvenPrinter(int maxNumber) {this.maxNumber = maxNumber;}public void printOdd() {synchronized (lock) {while (number <= maxNumber) {// 如果是偶数,就等待while (number % 2 == 0) {try {lock.wait();} catch (InterruptedException e) {Thread.currentThread().interrupt();return;}}// 打印奇数并增加if (number <= maxNumber) {System.out.println(Thread.currentThread().getName() + ": " + number);number++;}// 通知偶数线程lock.notifyAll();}}}public void printEven() {synchronized (lock) {while (number <= maxNumber) {// 如果是奇数,就等待while (number % 2 != 0) {try {lock.wait();} catch (InterruptedException e) {Thread.currentThread().interrupt();return;}}// 打印偶数并增加if (number <= maxNumber) {System.out.println(Thread.currentThread().getName() + ": " + number);number++;}// 通知奇数线程lock.notifyAll();}}}public static void main(String[] args) {OddEvenPrinter printer = new OddEvenPrinter(10);Thread oddThread = new Thread(printer::printOdd, "奇数线程");Thread evenThread = new Thread(printer::printEven, "偶数线程");oddThread.start();evenThread.start();try {oddThread.join();evenThread.join();} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.println("打印完成!");}
}
- 使用CountDownLatch 方法
这个方法允许一个或者多个线程等待其他线程完成操作。最终主线程继续执行。(问题,这玩意会阻塞主线程)
这是一个实现简单 CompletableFuture
- 使用CyclicBarrier 方法
也是一个同步辅助类,允许一组线程互相等待。知道所有线程都到到某个公共屏障点。
可以传递到达数量,以及到达后的操作
CyclicBarrier 和CountDownLatch对比
快速对比表格
特性 | CountDownLatch | CyclicBarrier |
---|---|---|
重置能力 | 不可重置,一次性使用 | 可重置,循环使用 |
等待机制 | 主线程等待工作线程 | 所有线程相互等待 |
计数方向 | 递减计数(countDown()) | 递增计数(await()) |
使用场景 | 主线程等待多个任务完成 | 多个线程相互等待 |
构造方法 | CountDownLatch(int count) | CyclicBarrier(int parties) |
异常处理 | 相对简单 | 需要处理BrokenBarrierException |
灵活性 | 相对简单 | 支持屏障动作(Runnable) |
适用场景总结
CountDownLatch 适用场景:
1.
启动顺序控制 - 主线程等待所有准备工作完成
- 任务完成检测 - 等待多个并行任务完成
- 资源初始化 - 等待所有资源初始化完成后开始服务
- 测试协调 - 等待所有测试线程准备就绪
CyclicBarrier 适用场景:
1.
多阶段任务 - 多个线程需要同步执行多个阶段
- 并行计算 - 等待所有计算单元完成当前阶段
- 数据分片处理 - 处理完一个数据分片后等待其他分片
- 游戏同步 - 多个玩家需要同步进行每个回合