前言
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. 线程安全的核心问题:谁能动冰箱?
现在,你们的公寓(进程)里有一个全体共享的冰箱(堆),大家都可以拿吃的,但如果没有规矩,就会:
- 数据乱了:你室友刚把牛奶倒进杯子,结果你顺手把牛奶扔了……崩溃!💥
- 数据丢了:你买了鸡腿放进去,但瞬间就不见了(另一个线程改了它)。
所以,我们得立规矩!
几种解决办法:
✅ 加锁(synchronized)
像是在冰箱上装了个锁🔒,谁先拿钥匙,谁就能操作,其他人得等。
synchronized (冰箱) {
// 只能一个人操作冰箱
}
但问题是,加锁会让大家排队,效率变低。🚶♂️🚶♀️🚶♂️
✅ 原子操作(Atomic类)
换一种思路:如果操作是一次性、不可分割的,就不会出问题,比如一个人拿食物时不能被打断。Java 里 AtomicInteger
就是这么做的。
✅ 线程局部存储(ThreadLocal)
干脆别共用冰箱,每人发个小冰箱!💡 ThreadLocal
让每个线程有自己的数据,互不干扰。
ThreadLocal<String> myFood = new ThreadLocal<>();
myFood.set("炸鸡🍗");
System.out.println(myFood.get()); // 每个线程都有自己的炸鸡!
4. 现实中的多线程环境
现在,主流操作系统都是多任务的(可以同时运行多个进程)。为了安全,每个进程的内存是独立的,进程之间互不干涉,防止"隔壁宿舍来抢吃的"(进程间数据访问)。
但进程内部,所有线程都能访问堆,这才是问题的根源!
所以,当多个线程同时改堆里的数据时,就可能会出线程安全问题,我们需要加锁、同步、或者用线程安全的结构来避免数据混乱。
5. 线程安全的核心点
- 栈(Stack)天生线程安全,堆(Heap)是问题根源。
- 线程安全本质上是"多线程访问共享资源"的问题。
- 可以通过加锁、原子操作、线程局部存储等方式来解决。
- 进程之间的内存是独立的,线程之间才会互相抢资源。
所以,线程安全就像共享冰箱的问题:如果大家都能同时随意操作,但又不出乱子,那就是真正的"线程安全"了!💪😆
最后的话
👉 如果这篇内容对你有帮助,别忘了点赞 👍、收藏 ⭐,还可以关注 🚀!
你的支持是我继续输出高质量内容的最大动力!💪 关注我,让多线程不再难懂!