Day04
1. Java线程有哪些状态?
- New:新建状态,线程对象刚被创建,还没调用start()启动。
- Runnable:就绪(可运行)状态,线程已启动,可能正在
行,也可能在等待CPU分配时间片。- BLOCKED:阻塞状态,线程正在等待获取一个监视器锁(synchronized的monitor)。
- WAITING:等待状态,线程无限期等待其他线程显示唤醒。
- TIMED_WAITING:计时等待状态,线程在一定时间内等待。
- TERMINATED:终止状态,线程执行完毕或异常退出。
NEW --> RUNNABLE --> (WAITING / TIMED_WAITING / BLOCKED) --> RUNNABLE --> TERMINATED
2. wait状态下的线程如何进行恢复到running状态?
线程处于 wait() 状态时,会释放对象锁并进入等待队列,底层实际上调用了类似 LockSupport.park() 的方法,使线程进入等待(WAITING)状态,线程此时没有占用CPU资源。
当其他线程调用同一个对象的 notify() 或 notifyAll() 时,会唤醒等待队列中的一个或全部线程。被唤醒的线程会尝试重新获取该对象的锁。
- 如果线程还没获取到锁,它会进入BLOCKED状态等待锁释放(此时线程实际上会调用 LockSupport.park() ,进入阻塞等待)。
- 当锁释放时,底层调用 LockSupport.unpark(thread) 给该线程“许可证”,线程解除阻塞,进入就绪状态,等待CPU调度继续执行恢复到running状态。
3. notify和notifyAll的区别?
在 Java 中,notify() 和 notifyAll() 都是用来唤醒在某个对象监视器上 wait() 的线程,但它们的行为有本质区别。
notify():只唤醒一个线程,其他线程仍处于等待状态。如果这个被唤醒的线程没有在适当时机再次调用 notify() 或 notifyAll(),其他线程就会一直卡在 wait() 里,陷入“永久等待”或“死锁风险”。
notifyAll():唤醒所有正在等待的线程,让它们全部退出 wait 状态,进入锁竞争状态。虽然只有一个线程能先拿到锁,但其余线程会在锁释放后继续依次被唤醒并执行,不会遗漏任何线程。
4. notify选择哪个线程?
notify() 方法无法指定唤醒哪个线程。它会在该对象的等待队列中,随即唤醒一个正在 wait() 的线程,具体选择由 JVM 实现决定,没有顺序也没有可控性。
5. 如何停止一个线程的运行?
- 异常法(中断 + 捕获异常):线程在阻塞状态(如sleep、wait、join)时,调用 interrupt() 会抛出 InterruptedException,通过异常来安全终止线程。
- 沉睡中停止(sleep + interrupt):如果线程正在sleep(),调用 interrupted() 会立即中断并抛出 InterruptedException,借此跳出循环或结束方法。
- return 判断停止(轮询中断标志):如果线程不是阻塞状态,需要在 run() 方法中不断检查 Thread.currentThread().isInterrupted(),一旦为 true 就使用 return 停止线程。
- stop() 暴力停止(已废弃):stop() 会直接终止线程,但它不会保证资源清理、安全释放锁等,容易引起数据不一致、死锁等问题。
推荐使用 interrupt() + 轮询/异常处理 的方式安全地终止线程,避免使用已废弃的 stop() 方法,以保障线程资源的完整性和程序的稳定性。
// 第一种
public void run() {try {while (true) {Thread.sleep(1000); // 阻塞中// 执行业务逻辑}} catch (InterruptedException e) {System.out.println("线程被中断,安全退出");}
}
// 第二种
Thread t = new Thread(() -> {while (true) {try {Thread.sleep(1000);} catch (InterruptedException e) {break; // 安全退出线程}}
});
// 第三种
public void run() {while (true) {if (Thread.currentThread().isInterrupted()) {return; // 退出线程}// 正常业务逻辑}
}
// 第四种
Thread t = new Thread(() -> {while (true) {// 可能操作共享资源}
});
t.stop(); // ❌ 强制终止
6. 调用 interrupt 是如何让线程抛出异常的?
每个线程都维护一个中断标志位(interrupt flag),初始值是 false。
当线程调用 interrupt() 方法时,并不会立刻终止线程,而是设置该线程的中断标志为 true。如果此时线程正处于阻塞状态(如调用了 sleep()、wait() 或 join()),则会立刻解除阻塞并抛出 InterruptedException 异常,同时中断标志被清除;如果线程处于非阻塞状态,则不会抛出异常,只是标志位变为 true,线程需要自行通过 isInterrupted() 方法轮询判断是否中断,从而安全退出。
因此,interrupt() 提供的是一种协作式中断机制,线程能否终止取决于自身是否正确响应中断信号。
7. 如果是靠变量来停止线程,缺点是什么?
- 无法中断阻塞状态:如果线程正在执行 sleep()、wait() 或 join() 等阻塞操作,设置变量并不能打断它,线程仍会卡在阻塞种,不能立即响应停止。
- 无法统一处理所有线程状态:变量方式只能控制运行中的线程,而不能处理阻塞、挂起、等待等状态,控制粒度不够细。
- 线程与外部耦合高:必须在多个地方显示地写 if(!flag) break;,容易遗漏判断,增加维护成本;线程任务代码被迫关注控制逻辑,不如 interrupt() 那样内聚清晰。
- 不符合Java线程中断机制规范:Java 推荐通过 interrupt() 配合异常或轮询中断标志来终止线程,这是更通用、安全、规范的做法。使用变量是“自造机制”,不利于与线程池等框架协作。
8. volatile保证原子性吗?
volatile 不保证原子性。它是 Java 提供的一种轻量级同步机制,具备以下三个特性:
- 保证可见性:一个线程修改 volatile 变量,其他线程能立即看到最新值。
- 不保证原子性:像 i++ 这样的复合操作仍然不是线程安全的。
- 禁止指令重排序:编译器和 CPU 不会把 volatile 相关操作与前后的指令重排。
我们可以使用以下方式保证原子性:
- 使用 synchronized 关键字对临界区加锁
- 使用 原子类,如 AtomicInteger 提供的原子操作方法
- 使用 显式锁,如 ReentrantLock 控制并发访问
9. synchronized支持重入吗?如何实现的?
synchronized 是支持重入的锁,这在 Java 中称为可重入锁(Reentrant Lock)。
支持重入指的是同一个线程在外层方法获得锁后,能够进入该锁保护的内层方法,不会因为再次请求锁而被阻塞。
synchronized 是靠对象监视器(Object Monitor)实现的。 每个对象有一个 monitor(监视器锁),当线程第一次获取该对象的锁,锁计数器 + 1,如果这个线程再次请求同一个锁(重入),计数器继续 + 1,直到退出 synchronized 块或方法时,计数器依次递减,归0时真正释放锁。
10. HTTP和HTTPS的区别
HTTP 是超文本传输协议,它采用的是明文传输,因此在通信过程中容易被窃听、篡改或伪造,存在严重的安全隐患。
HTTPS(HTTP Secure)是 HTTP 上加入了 SSL/TLS 安全层协议,通过加密机制来保障通信的机密性、完整性与身份验证。具体来说,它在 TCP 和 HTTP之间新增了一层安全协议,使用数据在传输过程中能够加密,防止信息泄露。
在连接建立方面:
- HTTP:只需 TCP 三次握手后即可直接进行数据通信。
- HTTPS:在 TCP 三次握手之后,还需进行一轮 SSL/TLS 握手过程,用来协商加密算法、验证身份并交换密钥,之后才能开始加密通信。
此外,两者的默认端口也不同:
- HTTP 使用端口 80。
- HTTPS 使用端口 443。
为了保证通信双方身份可信,HTTPS 还需要向 CA(证书颁发机构)申请数字证书,用于对服务器(甚至客户端)进行身份认证。
11. TCP三次握手
TCP 是一个面向连接的、可靠传输协议,它要求通信双方在正式传输数据前必须先建立连接。这个建立连接的过程就叫做三次握手。
- 客户端发送一个 SYN 报文(同步序列号)给服务端,这个报文里包含一个初始的序列号 seq = x,表示客户端要建立连接。
- 服务端收到请求后,确认连接请求,发送一个 SYN + ACK 报文,SYN = 1(表示也要建立连接),ACK = 1(表示对客户端的 SYN 进行确认),ack = x + 1,seq = y(服务端的初始序列号)。
- 客户端收到 SYN + ACK 报文,发送一个 ACK 报文作为响应,表示连接建立完成,ACK = 1,ack = y + 1。
经过三次握手,双方建立连接,就进入通信状态了。
12. 为什么TCP是三次握手而不是两次?
TCP 需要三次握手是为了确保通信的双方都具备接收和发送的能力,并且连接是可靠有效的。
如果只进行两次握手,服务端在发送完 SYN+ACK 后就认为连接建立成功,但此时客户端可能已经崩溃或并未收到响应。
更严重的是,如果之前某个延迟的 SYN 报文再次到达服务端,在两次握手的模型下,服务端会误以为是新的连接请求,造成伪连接和资源浪费。
第三次握手中,客户端回复 ACK,明确告诉服务端:“你的回应我收到了,连接可以正式建立”,这样才能保证连接的可靠性和防止资源泄露。
例子:假设只有两次,客户端第一次发了 S Y N 1 SYN_{1} SYN1,网络中延迟很久;客户端再次发了 S Y N 2 SYN_{2} SYN2,服务端收到了并回 S Y N + A C K {SYN + ACK} SYN+ACK,连接正常建立。一段时间后,网络延迟的 S Y N 1 SYN_{1} SYN1 到达服务端,服务端由于只有两次握手没有收到客户端的确认 A C K {ACK} ACK,无法区分这个 S Y N 1 SYN_{1} SYN1 是旧的,它直接认为这是一个新连接(其实是伪连接),于是向客户端发送 S Y N + A C K {SYN + ACK} SYN+ACK,分配资源等待数据接入,但客户端早已下线或关闭连接,根本不会回应,服务端就会把这个连接挂在半连接队列中(这时需要 TCP 的重传和超时机制去清理)。
13. TCP四次挥手
ESTABLISHED初始状态,此时连接已建立,通信正常进行。
- 客户端向服务端发送 FIN 报文,表示自己没有数据要发了(但还可以接收),进入 FIN_WAIT_1 状态。
- 服务端收到 FIN 后,发送 ACK,表示“我知道你要断了”,客户端进入 FIN_WAIT_2 状态,服务端进入 CLOSE_WAIT 状态。
- 服务端也发送 FIN,表示“我也没数据了,可以断了”,服务端进入 LAST_ACK 状态。
- 客户端收到服务端的 FIN,回复 ACK,客户端进入 TIME_WAIT状态(等待2倍最大报文段生命周期),然后才完全关闭服务端收到 ACK 后直接关闭连接(CLOSED)。