202508|技术日志
0804
Thread.interrupt()
导论
今天有一个需求是将原先单独部署的服务B迁移到现在的服务A中,A服务启动的时候顺带把B服务一起启动了,在服务B的核心启动逻辑中有这么一段:
1 |
|
Thread.currentThread().join()
这一行是将当前线程进行阻塞
因此不能直接串行式地照搬代码,否则会出现A服务启动一半开始启动服务B的时候,服务B确实启动了,然后就一直阻塞住了。
最佳实践的方式是:另起一个线程来启动服务B并进行阻塞
1 |
|
从系统资源安全性的角度出发,我们应当实现:当服务A停止之前,顺带把服务B先停止了
这个时候我们就需要用到 Thread.interrupt()
作用
threadA.interrupt() 方法用于向线程发送中断信号
从系统设计的角度出发,一个线程的停止(生命周期),不应该受其他线程的控制。因此在 Thread 类中 stop, destory 等方法都是被标记为过时的
也正因此,interrupt 并不保证会立刻终止线程,只是进行通知,通知线程应该被中断了
具体到底中断还是继续运行,应该由被通知的线程自己处理
1 |
|
真正的流程是,某个线程(比如 main 线程)调用了 某个线程 A 的 interrupt 方法,此时根据线程 A 所处的状态来决定后续的状态:
- 如果线程处于阻塞态,比如调用了
wait()
,join()
:阻塞的线程会立刻退出阻塞态并抛出InterruptedException
- 如果线程只是正常执行:线程只是修改当前 interrupt 的标记,不做任何操作
最佳实践
我们之前提到:
具体到底中断还是继续运行,应该由被通知的线程自己处理
这就是使用线程时的最佳实践:
- 如果线程执行逻辑有被中断的需求,那么就需要在运行时通过
Thread.interrupted()
检查当前的中断标志,如果已经中断自行跳转逻辑 - 在调用阻塞等方法时,尤其需要注意处理 throws 的
InterruptedException
,如果涉及到服务资源的,需要关闭资源;或是 catch 异常后结束线程逻辑
现在我们再看下面这些代码:
1 |
|
1 |
|
上面就是基于最佳实践编写的代码,其中第二部分在服务非阻塞启动后,阻塞当前线程,很好地处理了 InterruptedException
,在 finally 中释放资源
因此,回归最初导论中的问题:想实现当服务A停止之前,顺带把服务B先停止的操作
只需要在 A 服务中新增一个 hook
1 |
|
这里调用 webSocketThread.interrupt()
后,通知了跑着服务的B线程应该进行中断了。而B线程的实现逻辑中又catch了异常,在 finally 中停止了核心服务的资源
创建线程的方式
经常背八股的同学都知道,创建线程的方式有:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 使用ThreadPoolExecutor线程池
- …
我们来看代码,以实现 Runnable 接口为例
1 |
|
1 |
|
这样调用我们会发现结果实际上还是 main: run
这是因为你实现了 Runnable 接口,但选择直接调用 Runnable 实现类的 run 方法,本质上还是在主线程进行方法调用,和普通的方法调用并无本质区别
真正想要在另外一个线程打印,只有一个入口:调用 thread 对象实例的 start()
1 |
|
线程体 & 线程
我们上面说的八股中常见的什么继承 Thread 类等等操作,严格意义上来说是 创建线程体 而不是创建线程
线程体定义了线程执行时具体应该执行的任务,是线程执行任务的承载体,是一个接口(标准)
所以才需要我们继承 Thread 类重写 run 方法/ 实现 Runnable/Callable 接口,这些都是为了定义线程跑起来的时候应该做什么,本质上并没有创建线程
唯一创建线程的只有一个入口:调用 thread 对象实例的 start()
,只是我们可以用多种构造线程体的方式,在实例化 thread 对象的时候以多种形式向构造函数传参
想要让程序片段在另起的线程上跑,不管是 JDK 源码库的实现逻辑(例如使用线程池)还是我们之前提到的实现 Runnable 接口,本质上只有唯一一种,就是先实例化 Thread 类实例,然后调用 start()
0805
线程的生命周期
前置内容:需要了解原生的 synchronized
以及 Object.wait() / Object.notify()
synchronized
synchronized
的核心作用是给代码块或方法加锁,确保同一时间只有一个线程能执行被锁定的代码。它有两个关键特性:
- 互斥性:同一时间只有一个线程能持有锁(其他线程会阻塞等待)。
- 可见性:线程释放锁时,会将修改后的共享变量同步到主内存;其他线程获取锁时,会从主内存读取最新的变量值。
synchronized
的语法有两种:
- 修饰方法(如
synchronized void method()
):锁对象是当前实例(非静态方法)或类对象(静态方法)。 - 修饰代码块(如
synchronized(lockObj) { ... }
):锁对象是括号里的lockObj
(必须是一个对象,不能是基本类型)。
wait-notify
Object.wait() / Object.notify()
是 JDK 提供的多线程环境下线程的 等待————同步
机制,用于实现线程间的协作(比如一个线程等待某个条件,另一个线程满足条件后唤醒它)
需要注意的是必须在 synchronized
中定义,不然会抛出 IllegalMonitorStateException
wait()
:让当前线程释放锁并进入Waiting 状态(被动等待),直到被其他线程通过notify()
或notifyAll()
唤醒。其中释放锁是关键:如果不释放锁,其他线程永远无法获取锁,会导致死锁。notify()
:随机唤醒一个正在等待当前锁的线程(使其从 Waiting 状态退出),但被唤醒的线程不会立即执行,而是需要重新竞争获取锁(获取到锁后才会继续执行)。
测试demo
1 |
|
总结
简单来说就是:
- 线程创建后进入NEW状态
- 调用
thread.start()
后进入RUNNABLE状态,此时还需要等待OS的资源分配,例如CPU执行权。只不过JVM中把有CPU执行权和无CPU执行权统一都叫做RUNNABLE,没有在进行细分,只要在JVM中执行的都叫做RUNNABLE - 线程抢锁但是没有抢到的为BLOCKED状态
- 线程抢到了锁,但是通过调用
Object.wait() / Thread.join() / LockSupport.park()
来释放锁并主动进入WAITING态等待被唤醒,唤醒后不代表立刻执行,还是需要抢锁,因为可能有多个线程同时竞争 - 线程也可以通过调用
Thread.sleep() / Thread.jon(time) / Object.wait(time) / LockSupport.parkNanos() / LockSupport.parkUntil()
进入超时等待,时间到了会退出TIMED_WAITING,超时后自动唤醒,同样需要重新竞争锁才能继续执行 - 线程执行完所有逻辑或是抛出异常后自动进入TERMINATED状态
0806
pnpm 和 npm 区别
核心区别在于二者的依赖存储方式不同
- npm 采用 “扁平化 node_modules”,可能导致依赖版本混乱(同一依赖的不同版本可能被提升)。
- pnpm 采用 “内容可寻址存储”,通过硬链接和符号链接管理依赖,相同依赖只存一份,节省磁盘空间,且安装速度更快。
npm依赖提升
例如,我们现在的工程引入两个插件:toolA
和toolB
1 |
|
toolA:
1 |
|
toolB:
1 |
|
当执行npm install
后,npm 会尝试扁平化依赖:
- 由于
toolA
和toolB
依赖lodash
的不同版本,npm 会选择其中一个版本提升到顶层(假设是lodash@4.17.0
,因为版本更新); - 另一个版本(
lodash@3.10.0
)则保留在toolB
自己的node_modules
中(因为toolB
明确依赖 3.x)。
所以最终的目录结构如下:
1 |
|
如果toolA和toolB都定义了lodash
指定版本,那么都不会有问题
但是如果tooB忘记定义了,此时,toolB
引用lodash
时,npm 会默认去找顶层的lodash@4.17.0
,但4.x
中可能没有toolB依赖的代码版本(比如依赖的方法被删除了),执行的时候就会报错
因此如果使用 npm 时存在不同包依赖同一依赖的不同版本,则会导致:
- 若包未正确声明依赖,可能错误引用顶层的 “不兼容版本”;
- 依赖提升的不确定性可能导致部分包被迫使用非预期版本,引发兼容性问题。
pnpm符号连接
pnpm不采用依赖提升而是采用符号链接可以很好地解决上面的问题,如果采用 pnpm 那么最终的目录结构如下
1 |
|
还是上面出现问题的场景,如果 toolB 忘记在依赖中定义 lodash,那么 toolB 中的代码 require('lodash')
会直接失败(找不到模块),而不是像 npm 那样 “向上查找并意外引用顶层版本”。这强制开发者必须显式声明依赖,避免隐藏的版本问题。
0808
peerDependencies
peerDependencies
是 package.json
中的一个字段,用于声明当前包(通常是插件、工具库)与宿主环境(使用该包的项目)中其他依赖的兼容版本要求。
它的核心作用是:
- 告诉宿主项目:“我需要你提供这些依赖,且版本必须符合我的要求”
- 避免依赖重复安装(例如:React 插件不需要自己安装 React,而是使用宿主项目已有的 React)
- 确保兼容性(例如:插件明确要求 React ≥17,避免宿主使用 React 16 导致冲突)