前言
Java不秃,面试不慌!
欢迎来到这片 Java修炼场!这里没有枯燥的教科书,只有每日一更的 硬核知识+幽默吐槽,让你在欢笑中掌握 Java基础、算法、面试套路,摆脱“写代码如写诗、看代码如看天书”的困境。
记住: 代码会背叛你,但知识不会! 坚持积累,总有一天,HR会为你的八股文落泪,面试官会因你的算法沉默。
Thread vs Runnable:谁更适合卖票?
想象一下,你开了一家电影院,卖票的小哥叫 Thread,而 Runnable 只是个卖票规则的小册子。这两种方式都能卖票,但如果用错了方式,可能就会出现 “同一张票被卖两次” 的乌龙事件!😱
🌟 先说区别,别让概念把你绕晕
Thread(继承方式)
- 像是直接雇了个员工(继承
Thread
类)。 - 这个员工有自己的卖票逻辑(重写
run()
方法)。 - 但每次你招个新员工,他就带着自己的一摞票,互相不认识,卖自己的票!
Runnable(实现接口)
- 像是提供了一本卖票手册(实现
Runnable
接口)。 - 你可以雇好多个员工(
Thread
对象),但他们都用同一本手册(共享Runnable
实例)。 - 这样大家卖的是同一摞票,不会重复卖票。
🔍 看代码,别瞎猜
❌ 错误示范(用 Thread
,导致票卖超了)
public class Test {
public static void main(String[] args) {
new MyThread().start(); // 第一个员工
new MyThread().start(); // 第二个员工
}
static class MyThread extends Thread {
private int ticket = 5; // 每个员工各自带 5 张票
public void run() {
while (true) {
System.out.println("Thread ticket = " + ticket--);
if (ticket < 0) {
break;
}
}
}
}
}
😱 发生了什么?
new MyThread().start();
创建了 两个 独立的MyThread
对象,每个对象有 自己的一摞 5 张票。- 两个人一起卖,结果票就卖了 2×5=10 张,远远超标!
✅ 正确示范(用 Runnable
,确保所有员工共用一摞票)
public class Test2 {
public static void main(String[] args) {
MyThread2 mt = new MyThread2(); // 共享的卖票逻辑
new Thread(mt).start(); // 第一个员工
new Thread(mt).start(); // 第二个员工
}
static class MyThread2 implements Runnable {
private int ticket = 5; // 只有一摞票,所有人都在卖这 5 张票
public void run() {
while (true) {
System.out.println("Runnable ticket = " + ticket--);
if (ticket < 0) {
break;
}
}
}
}
}
🎉 发生了什么?
- 同一个
MyThread2
实例 被两个Thread
线程共享,大家都在卖同一摞票! - 不会出现多卖票的情况,数据才是同步的。
🛠 选谁好?
对比项 | Thread(继承) | Runnable(实现) |
---|---|---|
使用方式 | 继承 Thread ,每个线程自己玩自己的 | 实现 Runnable ,多个线程共享资源 |
数据是否共享 | 不共享,各管各的 | 共享,同一份数据 |
灵活性 | 低(Java 单继承,不能再继承别的类) | 高(可以实现多个接口) |
适用场景 | 需要独立执行的任务,比如下载文件 | 需要多个线程协同处理的任务,比如卖票 |
🛠️ 守护线程:程序界的“工具人”
想象你开了一家餐厅,非守护线程(用户线程) 就是厨师、服务员,他们负责干活;守护线程(Daemon Thread) 就像打扫卫生的机器人,默默地清理餐桌和地板。但是 如果餐厅关门(所有用户线程都结束了),打扫机器人也会被直接拔掉电源,不管它有没有扫完! 🧹⚡
🔍 守护线程的核心特点
- “你们忙,我服务” —— 守护线程的存在是为了帮助用户线程完成任务,比如 垃圾回收(GC) 就是一个典型的守护线程。
- “你们走,我也走” —— 当所有用户线程都结束,守护线程也会 毫不犹豫地被终结,不会等待任务完成。
- “不靠谱的小弟” —— 由于它随时可能被系统结束,所以 不要让它承担关键任务! 不然你的数据可能会莫名蒸发!
🚨 守护线程的坑
⚠️ 千万别让它干这些事!
- ❌ 处理重要数据(如数据库写入、文件IO)
→ 它可能在你存文件到一半时突然挂掉,你的数据就凉了…… - ❌ 执行关键业务逻辑
→ 如果任务必须完整执行,就不要交给守护线程! - ❌ 线程池默认不会用守护线程
→ Java 自带的ExecutorService
会把守护线程变成用户线程,所以如果想用后台线程,不要用 Java 线程池!
📌 守护线程的正确应用场景
✅ 什么时候用守护线程?
- 后台任务: 比如 GC 垃圾回收器,它在后台默默回收垃圾对象,不影响主程序运行。
- 监控系统: 比如 日志监控,当主线程运行时,它一直记录日志,主线程结束了,日志线程也该走人了。
- 心跳检测: 比如 定期检查服务器状态,如果服务器已经停机,这个检测线程也应该自动退出。
👀 代码示例
❌ 错误示范(不该用守护线程的场景)
public class DaemonWrongExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
// 模拟写入数据库的操作
System.out.println("正在向数据库写入数据...");
Thread.sleep(5000);
System.out.println("数据库写入成功!");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.setDaemon(true); // ❌ 把关键任务线程设成守护线程(错误)
thread.start();
System.out.println("主线程结束!");
}
}
🚨 可能发生的问题:
- 主线程瞬间结束,导致守护线程 直接被 JVM 终结,数据库可能 根本没写完!
✅ 正确示范(守护线程用于后台任务)
public class DaemonRightExample {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("🧹 后台任务:清理垃圾中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
daemonThread.setDaemon(true); // ✅ 设置为守护线程(合适)
daemonThread.start();
System.out.println("主线程运行 3 秒后结束...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程结束,守护线程也跟着拜拜!👋");
}
}
💡 运行结果:
主线程运行 3 秒后结束...
🧹 后台任务:清理垃圾中...
🧹 后台任务:清理垃圾中...
🧹 后台任务:清理垃圾中...
主线程结束,守护线程也跟着拜拜!👋
⚠️ 重点注意
必须在 start()
之前设置
thread.setDaemon(true); // ✅ 必须在 start() 之前,否则会报错!
thread.start();
守护线程创建的新线程也会是守护线程
Thread daemon = new Thread(() -> {
Thread subThread = new Thread(() -> System.out.println("我是守护线程的子线程!"));
subThread.start();
});
daemon.setDaemon(true);
daemon.start();
🧐 业务代码里没写线程,Java 会悄悄创建线程吗?
你在业务代码里 没有手动创建线程,但 Java 底层其实已经悄悄帮你创建了多个线程,即使你只是执行 最普通的增删改查(CRUD)、创建数组 这些基本操作!🤯
🚀 先看 Java 默认创建的线程
当你运行一个最普通的 Java 程序时,Java 默认会创建多个线程,而你只是在用其中的一部分。
可以用 Thread.getAllStackTraces().keySet()
这个方法看当前 JVM 里的所有线程,比如运行下面这段代码:
public class CheckThreads {
public static void main(String[] args) {
Thread.getAllStackTraces().keySet().forEach(thread ->
System.out.println(thread.getName() + " - " + thread.getState()));
}
}
在 没有手动创建线程 的情况下,输出会有类似:
Finalizer - WAITING
Reference Handler - WAITING
Signal Dispatcher - RUNNABLE
Attach Listener - RUNNABLE
main - RUNNABLE
这些是什么?
线程名 | 作用 |
---|---|
main | 主线程,所有业务代码默认运行在这个线程 |
Finalizer | 垃圾回收相关,用于回收不再使用的对象 |
Reference Handler | 处理弱引用,防止某些对象被 GC 过早清理 |
Signal Dispatcher | JVM 信号处理线程,比如 Ctrl+C 关闭进程 |
Attach Listener | 调试用的线程,允许外部工具连接 JVM |
💡 那些你以为没用线程,其实底层偷偷用了的场景
1️⃣ 数据库增删改查(CRUD)
👉 JDBC 默认不会创建新线程,但如果用了 数据库连接池(如 HikariCP、Druid),它会创建一批线程来管理数据库连接!
✅ 代码示例:
DataSource ds = new HikariDataSource();
Connection conn = ds.getConnection(); // 可能会用到连接池的线程
2️⃣ Java 里的 IO 操作
👉 你在写/读文件、网络请求时,某些底层实现会创建线程!
✅ 代码示例:
BufferedReader br = new BufferedReader(new FileReader("data.txt"));
String line = br.readLine();
- 普通的
FileReader
是同步的,不会用到额外的线程。 - 但
NIO
(非阻塞 IO)用的Selector
机制,会用到额外线程!
3️⃣ Spring 框架
如果你用了 Spring Boot,即使你没手动创建线程,Spring 可能已经偷偷帮你创建了一堆线程!
- Tomcat 线程池(默认 200 个线程)
- 数据库连接池(默认 10~50 个线程)
- 异步任务线程池(如果用了
@Async
) - 定时任务线程池(如果用了
@Scheduled
)
你 没写线程代码,但 Tomcat 其实创建了多个线程来处理 HTTP 请求!
@RestController
public class DemoController {
@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
}
4️⃣ Java 的 ForkJoinPool
Java 8 之后,很多操作 默认就用并行线程池,比如:
✅ parallelStream()
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.parallelStream().forEach(System.out::println);
这里就创建了多个线程,由 ForkJoinPool
处理!
🔍 总结:不手动创建线程 ≠ 没有线程
你写的代码 | Java 背后偷偷做的事情 |
---|---|
运行 main() 方法 | 创建多个 JVM 线程(GC、Finalizer 等) |
连接数据库 | 连接池创建多个数据库线程 |
读取文件 | 可能会有后台 IO 线程(如 NIO) |
HTTP 请求(Spring) | Tomcat/Netty 线程池处理请求 |
parallelStream() | ForkJoinPool 创建多线程 |
所以,Java 其实一直在帮你偷偷创建线程! 😆
只不过 你平时写的业务代码主要跑在 main
线程,而那些 底层的线程是 JVM 和框架在管理的。
如果你要 真正控制线程,那你就得 自己手动创建 Thread
或用线程池 了!🚀
❤️ 看到这里,给个赞不过分吧?😆
如果这篇文章帮你 打开了新世界大门,不妨点个赞 👍,关注一下 📌,
让更多人 告别线程盲区,不再以为 "不写线程 = 没线程" !💡
有问题欢迎 留言讨论,一起成为 Java 多线程的高手! 🚀