Java并发与面试-每日必看(9)

前言

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) 就像打扫卫生的机器人,默默地清理餐桌和地板。但是 如果餐厅关门(所有用户线程都结束了),打扫机器人也会被直接拔掉电源,不管它有没有扫完! 🧹⚡

🔍 守护线程的核心特点

  1. “你们忙,我服务” —— 守护线程的存在是为了帮助用户线程完成任务,比如 垃圾回收(GC) 就是一个典型的守护线程。
  2. “你们走,我也走” —— 当所有用户线程都结束,守护线程也会 毫不犹豫地被终结,不会等待任务完成。
  3. “不靠谱的小弟” —— 由于它随时可能被系统结束,所以 不要让它承担关键任务! 不然你的数据可能会莫名蒸发!

🚨 守护线程的坑

⚠️ 千万别让它干这些事!

  • ❌ 处理重要数据(如数据库写入、文件IO)
    → 它可能在你存文件到一半时突然挂掉,你的数据就凉了……
  • ❌ 执行关键业务逻辑
    → 如果任务必须完整执行,就不要交给守护线程!
  • ❌ 线程池默认不会用守护线程
    → Java 自带的 ExecutorService 会把守护线程变成用户线程,所以如果想用后台线程,不要用 Java 线程池!

📌 守护线程的正确应用场景

✅ 什么时候用守护线程?

  1. 后台任务: 比如 GC 垃圾回收器,它在后台默默回收垃圾对象,不影响主程序运行。
  2. 监控系统: 比如 日志监控,当主线程运行时,它一直记录日志,主线程结束了,日志线程也该走人了。
  3. 心跳检测: 比如 定期检查服务器状态,如果服务器已经停机,这个检测线程也应该自动退出。

👀 代码示例

❌ 错误示范(不该用守护线程的场景)

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 DispatcherJVM 信号处理线程,比如 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 多线程的高手! 🚀


    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包

    打赏作者

    Starry-Walker

    你的鼓励将是我创作的最大动力

    ¥1 ¥2 ¥4 ¥6 ¥10 ¥20
    扫码支付:¥1
    获取中
    扫码支付

    您的余额不足,请更换扫码支付或充值

    打赏作者

    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值