Java之线程基础详情篇!
开篇语
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
一、线程生命周期与状态转换
Java 里线程的状态是 Thread.State 这个枚举定义的,常见的就六个:
NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED
你可以把一个线程想象成人在公司上班的一天:刚入职、排队打卡、占用会议室、等别人开会、定时闹钟、最后下班走人。
1. NEW:刚创建,还没“开工”
Thread t = new Thread(() -> {
System.out.println("running...");
});
System.out.println(t.getState()); // NEW
- 只调用了
new Thread(...),还没start() - 仅仅是个“Java 对象”,还没有真正对应到底层系统线程
2. RUNNABLE:正在 CPU 上排队 / 跑着
start() 之后,线程进入 RUNNABLE:
t.start();
System.out.println(t.getState()); // 通常是 RUNNABLE
注意:
- 在 Java 里,
RUNNABLE包含了 “就绪 + 运行中” 两种状态 - 具体是正在 CPU 上跑,还是在就绪队列里等,JVM 不对你细分
典型进入 RUNNABLE 的路径:
- 从
NEW调用start() - 从
BLOCKED拿到锁 - 从
WAITING/TIMED_WAITING被唤醒且拿到锁 - 从 I/O 阻塞恢复(某些实现会表现为 RUNNABLE)
3. BLOCKED:等着进“临界区”的锁
BLOCKED 专门表示:
线程在等待一个 synchronized 的监视器锁(intrinsic lock)。
例如:
final Object lock = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock) {
try { Thread.sleep(100000); } catch (InterruptedException e) {}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("t2 got lock");
}
});
t1.start();
Thread.sleep(100); // 让 t1 先拿到锁
t2.start();
Thread.sleep(100);
System.out.println(t2.getState()); // 可能是 BLOCKED
这里:
t1先进入 synchronized 块,并sleep,一直占着锁t2想进 synchronized 块,发现锁被占用 →BLOCKED- 当
t1出 synchronized,t2才能从BLOCKED→RUNNABLE
4. WAITING:无限期等待某个条件
WAITING 表示:
线程在无限期等待,直到被别的线程显式唤醒。
常见导致 WAITING 的方法:
Object.wait()(无超时)Thread.join()(无超时)LockSupport.park()
例子(wait / notify):
final Object lock = new Object();
Thread t = new Thread(() -> {
synchronized (lock) {
try {
lock.wait(); // 线程进入 WAITING,并且释放锁
System.out.println("被唤醒啦");
} catch (InterruptedException e) {}
}
});
t.start();
Thread.sleep(100);
System.out.println(t.getState()); // WAITING
synchronized (lock) {
lock.notify();
}
特点:
- 线程会释放锁(和
BLOCKED不同) - 必须等到别人
notify()/notifyAll()/ interrupt 才可能继续
5. TIMED_WAITING:带闹钟的等待
TIMED_WAITING 表示:
线程在有“时间上限”的等待:时间到了会自动醒。
典型来源:
Thread.sleep(millis)Object.wait(timeout)- 带超时的
Thread.join(timeout) LockSupport.parkNanos()/parkUntil()
例子:
Thread t = new Thread(() -> {
try {
Thread.sleep(10000); // 10 秒
} catch (InterruptedException e) {}
});
t.start();
Thread.sleep(100);
System.out.println(t.getState()); // TIMED_WAITING
- 超时到了,线程从
TIMED_WAITING→RUNNABLE - 如果是
wait(timeout),还需要重新拿锁才能继续执行
6. TERMINATED:线程结束了
当 run() 或 call() 执行完,线程就进入 TERMINATED:
Thread t = new Thread(() -> {
System.out.println("done");
});
t.start();
t.join(); // 等它结束
System.out.println(t.getState()); // TERMINATED
7. 状态转换小总结(文字版“状态图”)
可以把线程的一生理解为:
-
NEW→(start)→RUNNABLE -
RUNNABLE→- 进入
synchronized前抢锁失败 →BLOCKED - 调用
wait()/join()/park()→WAITING - 调用
sleep()/wait(timeout)/join(timeout)/parkNanos→TIMED_WAITING
- 进入
-
BLOCKED/WAITING/TIMED_WAITING→- 拿到锁、被唤醒、超时、被中断 等 → 回到
RUNNABLE
- 拿到锁、被唤醒、超时、被中断 等 → 回到
-
最终,run 执行完 →
TERMINATED
二、Thread vs Runnable vs Callable:谁负责干活?谁只是“任务说明书”?
你可以这么理解:
Runnable/Callable:任务说明书Thread:真正的“工人”- 线程池(ExecutorService):工头 + 员工队列
1. Thread:线程本体 + 可选“任务”
Thread 是一个类,代表一个线程对象,它内部有一个 run() 方法:
- 可以直接继承
Thread,重写run() - 也可以把
Runnable丢进去,让它帮你跑
示例 1:继承 Thread(不太推荐)
class MyThread extends Thread {
@Override
public void run() {
System.out.println("I am a Thread");
}
}
new MyThread().start();
问题:类已经被占用,不能再继承其它父类,可扩展性较差。
2. Runnable:没有返回值的“任务接口”
Runnable 是一个函数式接口,只定义了一个 void run():
Runnable task = () -> {
System.out.println("running in " + Thread.currentThread().getName());
};
new Thread(task).start();
特点:
- 没有返回值
- 不能抛 checked exception(只能自己 try-catch)
- 适合那些只需要“干完就完了”的任务
在使用线程池时,execute(Runnable command) 就是跑这种任务。
3. Callable:有返回值的任务接口(配合 Future)
Callable<V> 是比 Runnable 更“高级”一点的任务描述:
- 方法是
V call() throws Exception; - 有返回值
- 可以抛 checked exception
典型用法:配合 ExecutorService.submit() 和 Future 使用:
import java.util.concurrent.*;
Callable<Integer> task = () -> {
Thread.sleep(1000);
return 42;
};
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> future = pool.submit(task);
System.out.println("doing something else...");
Integer result = future.get(); // 阻塞等待结果
System.out.println("result = " + result);
pool.shutdown();
对比一下:
| 对象 | 类型 | 核心方法 | 返回值 | 是否能抛 checked 异常 | 常见使用场景 |
|---|---|---|---|---|---|
Thread |
类 | run() / start() |
void |
不支持(run 不能抛) | 直接表示一个线程 |
Runnable |
函数式接口 | run() |
void |
不支持 | 提交到线程池、给 Thread 跑 |
Callable |
函数式接口 | call() |
任意类型 V |
支持 | 有返回值、有异常的异步任务 |
一句直白总结:
- 想要返回值?用
Callable+Future。 - 不需要返回值,只是跑一下逻辑?
Runnable就够了。 - 不要随便 new Thread,优先线程池。
三、sleep() vs wait():都“睡觉”,但一个只是打盹,一个是“放弃座位排队重来”
这俩名字都很迷惑新人,但本质区别非常关键。
1. 来个对比大表先压压惊
| 对比点 | Thread.sleep() |
Object.wait() |
|---|---|---|
| 所在类 | Thread |
Object |
| 是否释放锁 | ❌ 不释放锁 | ✅ 释放对象监视器锁(synchronized 的那个锁) |
| 是否必须在 synchronized 里调用 | ❌ 不要求 | ✅ 必须持有该对象的 monitor,否则抛 IllegalMonitorStateException |
| 唤醒方式 | 时间到了 / 被中断 | notify() / notifyAll() / 时间到 / 被中断 |
| 典型用途 | 单纯让线程暂停一段时间,不用于通信 | 线程间协作 / 条件等待(生产者-消费者等) |
| 所属状态 | 通常进入 TIMED_WAITING |
WAITING 或 TIMED_WAITING |
2. sleep():谁也不管,我就静静躺一会儿
try {
Thread.sleep(1000); // 当前线程睡 1 秒
} catch (InterruptedException e) {
// 响应中断
}
特点:
- 当前线程进入
TIMED_WAITING - 不会释放已经占有的任何锁(比如你在 synchronized 里 sleep)
- 时间到后自动恢复到
RUNNABLE - 中途被
interrupt()会抛InterruptedException,并清除中断标志
比如:
synchronized (lock) {
Thread.sleep(10000); // 期间别人拿不到这个 lock
}
这就是为什么在 synchronized 里长时间 sleep() 是个风险操作:
你拿着锁睡觉,别人全被堵在外面。
3. wait():把锁先让出来,等别人叫我再继续
wait() 是配合 synchronized 使用的线程协作机制。
final Object lock = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("t1: wait before");
lock.wait(); // 1. 释放 lock
System.out.println("t1: resumed");
} catch (InterruptedException e) {}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("t2: notify");
lock.notify();
}
});
关键行为:
-
调用
lock.wait()前,当前线程必须已经持有lock的锁(在 synchronized 块/方法中) -
调用
wait()后:- 当前线程进入
WAITING或TIMED_WAITING - 释放该对象的锁,让别的线程可以拿到这个锁干活
- 当前线程进入
-
其他线程在同一个对象锁上调用
notify()/notifyAll()后:- 等待中的线程会被唤醒,重新去抢这个锁
- 抢到锁之后,从
wait()之后继续执行
4. 一个经典生产者-消费者对比示例
使用 wait() / notify() 实现简单阻塞队列
class MyBlockingQueue {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity;
public MyBlockingQueue(int capacity) {
this.capacity = capacity;
}
public void put(int value) throws InterruptedException {
synchronized (queue) {
while (queue.size() == capacity) {
queue.wait(); // 队列满了,释放锁,等待消费者消费
}
queue.add(value);
queue.notifyAll(); // 通知可能在等的消费者
}
}
public int take() throws InterruptedException {
synchronized (queue) {
while (queue.isEmpty()) {
queue.wait(); // 队列空了,释放锁,等生产者放数据
}
int val = queue.poll();
queue.notifyAll(); // 通知可能在等的生产者
return val;
}
}
}
这里用的是典型的 条件等待 + 释放锁 的模式,sleep() 完全干不了这事。
如果你用 sleep() 来轮询队列是否为空,不仅浪费 CPU(频繁检查),还不精确(时间控制难)。
5. sleep() vs wait() 再来几句“人话”版总结
-
sleep():“我就在这儿等一会儿,谁也别动我的东西(锁),时间到了我自己继续跑。”
-
wait():“我先把座位(锁)让出来,大家先忙,等你们叫我(notify)我再抢座回来继续干。”
所以:
- 想要简单延时:
sleep() - 想要线程之间协作 / 条件同步:
wait()/notify()(或更现代的 Lock / Condition / BlockingQueue 等)
四、最后帮你串一下三块知识
如果把刚才三块揉成一个脑图:
-
线程状态
- 启动:
NEW → RUNNABLE - 等锁:
BLOCKED(synchronized) - 等条件 / 等别人叫:
WAITING / TIMED_WAITING(wait/join/park/sleep) - 结束:
TERMINATED
- 启动:
-
任务 vs 线程
Runnable/Callable= 任务描述Thread= 真正线程- 线程池 = 管线程 + 执行任务
-
sleep vs wait
- 都会让线程进入
TIMED_WAITING - 最大差别:是否释放锁 & 是否需要别人“叫醒”
sleep:不管别人,就自己暂停wait:用于线程协调,释放锁,配合notify/notifyAll
- 都会让线程进入
… …
文末
好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。
… …
学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!
wished for you successed !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!
- 点赞
- 收藏
- 关注作者
评论(0)