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

前言

Java不秃,面试不慌!
欢迎来到这片 Java修炼场!这里没有枯燥的教科书,只有每日一更的 硬核知识+幽默吐槽,让你在欢笑中掌握 Java基础、算法、面试套路,摆脱“写代码如写诗、看代码如看天书”的困境。
记住: 代码会背叛你,但知识不会! 坚持积累,总有一天,HR会为你的八股文落泪,面试官会因你的算法沉默。


线程的⽣命周期?线程有⼏种状态

  想象一下,线程就像一个上班族,他的一天(或者说一生)就是他的生命周期。线程的状态可以分为五种,就像是一个社畜从面试到退休的全过程。来,咱们聊聊这家伙的悲(bei)惨(can)人生吧。


1. 新建状态(New):简历投递,等待入职

线程刚被创建时,就像是你刚投了简历,HR(JVM)已经知道你了,但还没让你入职。所以这个时候你虽然存在,但不能干活。

Thread worker = new Thread(() -> System.out.println("打工人,打工魂!"));

此时这个 worker 线程还在“简历库”里,没人理你。

2. 就绪状态(Runnable):成功入职,但摸鱼等待排班

当别的线程(通常是主线程)调用 start() 方法,你就正式入职了!但别高兴太早,这不意味着你马上开始干活,你只是被公司(CPU)认定可以工作了,具体什么时候轮到你得看排班(CPU 调度)。

worker.start();

此时 worker 线程进入了就绪状态,等着 CPU 来安排你上岗。


3. 运行状态(Running):上班打卡,正式干活

当 CPU 终于轮到你,你就进入了运行状态,开始执行任务。

public void run() {
    System.out.println("今天也是搬砖的一天!");
}

但是,这可不是你说了算的,你干着干着,CPU 可能就会让你滚去歇着(被暂停),然后让别的线程来工作。这就是打工人的宿命。


4. 阻塞状态(Blocked):被老板叫去开会,工作暂停

有时候,你正在干活,结果突然有个资源(比如某个对象的锁)被别的同事占用了,咋办?你只能乖乖等着,这就是同步阻塞

synchronized (this) {
    System.out.println("正在使用公司打印机……");
}

如果别的线程(同事)已经在用这个 synchronized 资源(比如打印机),你只能站旁边干等,等前面那位打印完,你才能继续。

阻塞的情况还有几种,比如:

  • 等待阻塞:就像你去休假(调用 wait()),但想回来上班还得等老板(其他线程)喊你 notify()
  • 其他阻塞:比如 sleep()(午休)或者 join()(等着别的同事汇报工作)。

5. 死亡状态(Dead):离职或被炒鱿鱼

线程最终要么工作完了,要么因为异常崩溃了,这时候,它的生命周期就结束了,进入了死亡状态,正式从公司毕业(或者被裁)。

System.out.println("我太累了,不干了!");

线程一旦进入死亡状态,就不能再重新上岗(不能再 start() 了)。


sleep()、wait()、join()、yield()之间的的区别

想象一下,咱们几个朋友约着一起开黑打游戏,结果各种幺蛾子让大家的“游戏状态”千奇百怪。现在,咱们用这个场景来聊聊 sleep()wait()join()yield() 的区别,让复杂的概念简单点!


1. sleep():强行睡觉,不管别人

(场景:你突然累了,自己去睡觉,别人喊你也没用,等睡醒再继续玩。)

Thread.sleep(3000); // 睡3秒
  • 谁提供的? Thread 类的静态方法,直接让当前线程睡觉。
  • 锁呢? 不会释放锁,谁也别想抢。
  • 干嘛用? 让当前线程暂时休息一会,等时间到了自己醒。
  • 能被叫醒吗? 除非有人用 interrupt() 强行打断,否则就得睡够时间才能继续。

举个例子:

Thread t1 = new Thread(() -> {
    try {
        System.out.println("我困了,先睡3秒...");
        Thread.sleep(3000);
        System.out.println("睡醒了,继续!");
    } catch (InterruptedException e) {
        System.out.println("谁打断我睡觉?!");
    }
});
t1.start();

2. wait():乖乖等通知,释放资源

(场景:你想等朋友一起打游戏,于是乖乖坐着等,顺便把电脑让出来,直到有人拍你肩膀说“可以开黑了”你才继续。)

synchronized (obj) {
    obj.wait();
}
  • 谁提供的? Object 类的方法,必须配合 synchronized 使用。
  • 锁呢? 释放锁,让别人用这个资源。
  • 干嘛用? 让线程进入“等待池”,等 notify()notifyAll() 唤醒后才能继续。
  • 能被叫醒吗? 需要别人 notify() 你,或者 notifyAll() 让所有人都去竞争资源。

举个例子:

Object lock = new Object();

Thread t1 = new Thread(() -> {
    synchronized (lock) {
        try {
            System.out.println("线程1:我要等通知...");
            lock.wait();
            System.out.println("线程1:终于等到通知了,继续执行!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});

Thread t2 = new Thread(() -> {
    synchronized (lock) {
        System.out.println("线程2:给你发通知!");
        lock.notify();
    }
});

t1.start();
Thread.sleep(1000);
t2.start();


3. join():等别人干完活再轮到你

(场景:你要等朋友B打完一局游戏,才能轮到你上场。)

t1.join();
  • 谁提供的? Thread 类的方法。
  • 锁呢? 不涉及锁,就是单纯等。
  • 干嘛用? 让当前线程等指定线程跑完再继续。
  • 能被叫醒吗? 只能等 join() 线程执行完,或者它被 interrupt() 终止。
Thread t1 = new Thread(() -> {
    try {
        Thread.sleep(3000);
        System.out.println("线程1:我先跑完!");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

t1.start();
t1.join();  // 必须等 t1 执行完,主线程才能继续

System.out.println("主线程:终于轮到我了!");

结果:必须等 t1 线程执行完,主线程才会继续。


4. yield():主动让出CPU,但还在队列里

(场景:你本来在玩游戏,突然良心发现,说“我让让别人玩一下”,但你依然留在游戏房间里,等下可能还是你上场。)

Thread.yield();
  • 谁提供的? Thread 类的方法。
  • 锁呢? 不影响锁,单纯让出CPU。
  • 干嘛用? 让当前线程放弃CPU时间,但不一定会被调度出去,可能下一轮还是它执行。
  • 能被叫醒吗? 这个不涉及“叫醒”,因为它只是自愿让出,还可能被调度回来。

举个例子:

Thread t1 = new Thread(() -> {
    for (int i = 0; i < 5; i++) {
        System.out.println("线程1:我先执行 " + i);
        Thread.yield(); // 主动让出CPU
    }
});

Thread t2 = new Thread(() -> {
    for (int i = 0; i < 5; i++) {
        System.out.println("线程2:我来抢一下 " + i);
    }
});

t1.start();
t2.start();

结果:线程1可能会让出CPU给线程2,但不一定,因为调度是随机的。

总结对比

方法作用是否释放锁是否需要 synchronized是否要等别人叫醒用法场景
sleep()休息一会再继续❌ 不释放❌ 不需要❌ 自动醒让线程短暂休眠
wait()等别人通知再继续✅ 释放锁✅ 需要✅ 需要 notify()线程间通信,等待条件满足
join()等某个线程先执行完❌ 不涉及❌ 不需要❌ 等完自动继续让某个线程必须跑完后再继续
yield()自愿让出CPU,但可能马上又抢回来❌ 不释放❌ 不需要❌ 直接回到就绪队列低优先级线程让出时间

线程安全:谁动了我的奶酪?🐭

想象一下,你和几个室友住在一个公寓里(进程),你们有一个共享的冰箱(堆),但每个人也有自己的小抽屉(栈)。现在问题来了:如果大家都能随意拿冰箱里的食物,会不会打架?🤔 这就是线程安全的问题!


1. 什么是线程安全?

如果多个线程(室友)都能同时访问一个对象(冰箱),但不会因为抢食而吵架(数据错乱、崩溃),那么这个对象就是线程安全的

线程安全 = 多个线程同时访问数据,结果仍然正确。

比如,你们有一个食物登记本(加锁机制),每次拿吃的前都要先登记,这样就不会出现 "谁偷吃了我的酸奶?" 的问题。


2. 堆 vs. 栈:公用冰箱 vs. 个人抽屉

存储类型是什么?谁能访问?安全吗?
堆(Heap)共享内存,所有线程都能用所有线程不安全,需要同步控制
栈(Stack)每个线程的独立空间只属于某个线程安全,互不干扰

堆(Heap):共享食物的公用冰箱

  • 所有人都能访问,但如果没有规则,可能会乱成一团(数据不一致)。
  • 在 Java 里,所有对象实例和数组都存在堆里。
  • 如果某个线程(室友)不断往里面塞东西但不清理(不归还内存),会导致 内存泄漏(冰箱爆满)。

栈(Stack):每个人的小抽屉

  • 每个线程都有自己的栈,互不影响,天生线程安全
  • 里面存的是局部变量方法调用信息
  • 线程切换时,操作系统会自动切换栈,完全不用操心(不需要手动管理内存)。

3. 线程安全的核心问题:谁能动冰箱?

现在,你们的公寓(进程)里有一个全体共享的冰箱(堆),大家都可以拿吃的,但如果没有规矩,就会:

  1. 数据乱了:你室友刚把牛奶倒进杯子,结果你顺手把牛奶扔了……崩溃!💥
  2. 数据丢了:你买了鸡腿放进去,但瞬间就不见了(另一个线程改了它)。

所以,我们得立规矩!

几种解决办法:

加锁(synchronized)
像是在冰箱上装了个锁🔒,谁先拿钥匙,谁就能操作,其他人得等。

synchronized (冰箱) {  
    // 只能一个人操作冰箱  
}

但问题是,加锁会让大家排队,效率变低。🚶‍♂️🚶‍♀️🚶‍♂️

原子操作(Atomic类)
换一种思路:如果操作是一次性、不可分割的,就不会出问题,比如一个人拿食物时不能被打断。Java 里 AtomicInteger 就是这么做的。

线程局部存储(ThreadLocal)
干脆别共用冰箱,每人发个小冰箱!💡 ThreadLocal 让每个线程有自己的数据,互不干扰。

ThreadLocal<String> myFood = new ThreadLocal<>();
myFood.set("炸鸡🍗");
System.out.println(myFood.get()); // 每个线程都有自己的炸鸡!

4. 现实中的多线程环境

现在,主流操作系统都是多任务的(可以同时运行多个进程)。为了安全,每个进程的内存是独立的,进程之间互不干涉,防止"隔壁宿舍来抢吃的"(进程间数据访问)。

进程内部,所有线程都能访问堆,这才是问题的根源!

所以,当多个线程同时改堆里的数据时,就可能会出线程安全问题,我们需要加锁、同步、或者用线程安全的结构来避免数据混乱。


5. 线程安全的核心点

  • 栈(Stack)天生线程安全,堆(Heap)是问题根源。
  • 线程安全本质上是"多线程访问共享资源"的问题。
  • 可以通过加锁、原子操作、线程局部存储等方式来解决。
  • 进程之间的内存是独立的,线程之间才会互相抢资源。

所以,线程安全就像共享冰箱的问题:如果大家都能同时随意操作,但又不出乱子,那就是真正的"线程安全"了!💪😆


最后的话

👉 如果这篇内容对你有帮助,别忘了点赞 👍、收藏 ⭐,还可以关注 🚀!
你的支持是我继续输出高质量内容的最大动力!💪 关注我,让多线程不再难懂!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Starry-Walker

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

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

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

打赏作者

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

抵扣说明:

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

余额充值