【JUC】线程
一、线程的状态及流转
Java 中线程状态定义在 Thread.State 枚举类中,一共有 6 种状态:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
可以这样理解:
1. NEW:新建状态
当你只是创建了线程对象,但还没有调用 start() 方法时,线程处于 NEW 状态。
Thread t = new Thread();
此时只是 JVM 中有了一个线程对象,真正的操作系统线程还没有启动。
2. RUNNABLE:可运行状态
当调用了 start() 方法之后,线程进入 RUNNABLE 状态。
t.start();
注意,Java 中没有单独区分“就绪”和“运行”。
在操作系统层面,线程可能有两种情况:
就绪:等待 CPU 调度
运行:正在 CPU 上执行
但是在 Java 的 Thread.State 中,这两种情况都统一叫:
RUNNABLE
所以可以理解为:
调用 start()
NEW --------------> RUNNABLE
抢到 CPU 执行权
RUNNABLE ---------> 正在运行
CPU 时间片用完
正在运行 ---------> RUNNABLE
但是 Java 代码里查看状态时,看到的还是 RUNNABLE。
3. BLOCKED:阻塞状态
当线程想进入 synchronized 修饰的代码块或方法,但是锁被其他线程持有时,就会进入 BLOCKED 状态。
例如:
synchronized (lock) {
// 临界区代码
}
假设 A 线程已经拿到了 lock 锁,B 线程也想进入这段代码,那么 B 线程就会进入:
BLOCKED
等 A 线程释放锁之后,B 线程才有机会重新进入 RUNNABLE 状态。
重点:
BLOCKED 是等待 synchronized 锁。
4. WAITING:无限等待状态
线程调用某些方法后,会进入无限等待状态,直到被其他线程唤醒。
常见方法有:
Object.wait();
Thread.join();
LockSupport.park();
比如:
lock.wait();
线程会释放锁,并进入 WAITING 状态,直到其他线程调用:
lock.notify();
lock.notifyAll();
再比如:
bThread.join();
A 线程调用 bThread.join() 后,会等待 B 线程执行完毕。此时 A 线程也会进入等待状态。
重点:
WAITING 是无限等待,必须等别人唤醒或等待的线程结束。
5. TIMED_WAITING:计时等待状态
线程调用带有时间参数的方法时,会进入 TIMED_WAITING 状态。
常见方法:
Thread.sleep(1000);
Object.wait(1000);
Thread.join(1000);
LockSupport.parkNanos();
LockSupport.parkUntil();
比如:
Thread.sleep(2000);
线程会睡眠 2 秒,2 秒后自动回到 RUNNABLE 状态。
重点:
TIMED_WAITING 是有限时间等待,时间到了可以自动恢复。
6. TERMINATED:终止状态
当线程的 run() 方法执行完毕,或者执行过程中抛出未处理异常,线程就会进入终止状态。
public void run() {
System.out.println("执行任务");
}
执行完之后:
RUNNABLE -> TERMINATED
线程结束后,不能再次调用 start()。
如果再次调用:
t.start();
t.start();
会报错:
IllegalThreadStateException
二、线程状态流转总结
可以这样记:
创建线程对象
NEW
调用 start()
NEW -> RUNNABLE
抢到 CPU
RUNNABLE -> 运行中
但是 Java 中仍然显示为 RUNNABLE
等待 synchronized 锁
RUNNABLE -> BLOCKED
调用 wait/join/park
RUNNABLE -> WAITING
调用 sleep/wait(time)/join(time)
RUNNABLE -> TIMED_WAITING
run 方法执行结束
RUNNABLE -> TERMINATED
面试中可以这样回答:
Java 线程在 Thread.State 中定义了六种状态,分别是 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。线程创建后是 NEW,调用 start 后进入 RUNNABLE。Java 中的 RUNNABLE 包含操作系统层面的就绪和运行。线程竞争 synchronized 锁失败会进入 BLOCKED,调用 wait、join 等方法会进入 WAITING,调用 sleep 或带时间参数的 wait、join 会进入 TIMED_WAITING,run 方法执行结束后进入 TERMINATED。
三、创建线程的方式
方式一:继承 Thread 类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行");
}
}
public class Demo {
public static void main(String[] args) {
new MyThread().start();
}
}
特点:
简单直接,但是 Java 是单继承,继承 Thread 后就不能继承其他类。
方式二:实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行");
}
}
public class Demo {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}
}
特点:
更推荐,任务和线程分离,避免单继承限制。
也可以用 Lambda:
new Thread(() -> {
System.out.println("线程执行");
}).start();
方式三:实现 Callable 接口结合 FutureTask
Runnable 没有返回值,也不能直接抛出受检异常。
如果任务需要返回结果,可以使用 Callable。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class Demo {
public static void main(String[] args) throws Exception {
Callable<Integer> callable = () -> {
return 100;
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
Integer result = futureTask.get();
System.out.println(result);
}
}
特点:
适合需要返回值的异步任务。
futureTask.get() 会阻塞当前线程,直到任务执行完并返回结果。
方式四:通过线程池创建线程
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(3);
pool.execute(() -> {
System.out.println("线程池执行任务");
});
pool.shutdown();
}
}
特点:
实际开发中更推荐使用线程池。
因为线程池可以:
复用线程
减少频繁创建和销毁线程的开销
统一管理线程资源
提高系统稳定性
四、多线程的应用场景
多线程适合处理一些耗时任务,避免主线程被长时间阻塞。
比如:
文件拷贝
文件上传下载
加载大量资源
网络请求
聊天软件收发消息
后台服务器处理多个请求
定时任务
日志异步写入
消息队列消费
举个例子:
如果一个服务器只有一个线程,那么同时来了 100 个请求,只能一个一个处理。
使用多线程后,可以多个请求同时被处理,提高并发能力。
五、A 线程启动 B 线程,A 要等 B 执行完再继续,怎么实现?
这个问题的核心是:
线程之间的协作 / 等待另一个线程执行完成
常见方法有三种:
Thread.join()
CountDownLatch
CompletableFuture
方法一:Thread.join()
代码
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread bThread = new Thread(() -> {
System.out.println("B 线程开始执行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B 线程执行完毕");
});
bThread.start();
bThread.join();
System.out.println("A 线程继续执行");
}
}
原理
A 线程调用:
bThread.join();
意思是:
A 等待 bThread 执行结束。
所以执行流程是:
A 线程启动
A 创建 B 线程
A 调用 bThread.start()
B 开始执行
A 调用 bThread.join(),A 阻塞等待
B 执行完毕
A 从 join() 返回
A 继续执行
执行顺序:
B 线程开始执行
B 线程执行完毕
A 线程继续执行
注意:
bThread.join();
阻塞的是调用 join 的线程,也就是 A 线程,不是 B 线程。
方法二:CountDownLatch
CountDownLatch 可以理解为一个倒计时计数器。
代码
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
Thread bThread = new Thread(() -> {
System.out.println("B 线程开始执行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B 线程执行完毕");
latch.countDown();
});
bThread.start();
latch.await();
System.out.println("A 线程继续执行");
}
}
原理
CountDownLatch latch = new CountDownLatch(1);
表示需要等待 1 个任务完成。
A 线程调用:
latch.await();
意思是:
只要计数器不为 0,A 就一直等。
B 线程执行完之后调用:
latch.countDown();
意思是:
计数器减 1。
计数器从 1 变成 0 后,A 线程就会被唤醒,然后继续执行。
流程:
A 创建 CountDownLatch,计数器为 1
A 启动 B 线程
A 调用 await(),等待计数器变成 0
B 执行任务
B 执行完调用 countDown()
计数器变成 0
A 被唤醒
A 继续执行
CountDownLatch 适合什么场景?
join() 适合等待某一个具体线程。
CountDownLatch 更适合等待多个任务完成。
例如 A 线程要等 B、C、D 三个线程都执行完:
CountDownLatch latch = new CountDownLatch(3);
每个线程执行完:
latch.countDown();
A 线程:
latch.await();
只有计数器变成 0,A 才继续。
方法三:CompletableFuture
CompletableFuture 是 Java 8 引入的异步编程工具。
代码
import java.util.concurrent.CompletableFuture;
public class CompletableFutureDemo {
public static void main(String[] args) {
System.out.println("A 线程启动 B 线程");
CompletableFuture.runAsync(() -> {
System.out.println("B 线程执行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B 线程执行完毕");
}).join();
System.out.println("A 线程继续执行");
}
}
原理
CompletableFuture.runAsync(() -> {
// B 线程执行的任务
})
这行代码会把任务提交到默认线程池中异步执行。
默认使用的是:
ForkJoinPool.commonPool()
也就是说,B 任务不是由 A 线程执行,而是由线程池里的其他线程执行。
然后:
.join();
表示:
A 线程等待这个异步任务执行完成。
所以流程是:
A 线程启动
A 提交异步任务
线程池中的某个线程执行 B 任务
A 调用 join() 等待异步任务完成
B 任务执行完毕
A 继续执行
CompletableFuture 中的 join 和 get 区别
CompletableFuture 也可以用:
future.get();
例如:
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("B 线程执行");
});
future.get();
区别是:
get() 需要处理受检异常:InterruptedException、ExecutionException
join() 不需要显式处理受检异常,会把异常包装成 CompletionException
所以:
future.join();
写起来更简单。
六、三种方式怎么选?
可以这样总结:
| 方法 | 适合场景 | 特点 |
|---|---|---|
Thread.join() |
A 等待一个具体线程 B 执行完 | 简单直接 |
CountDownLatch |
A 等待一个或多个线程/任务完成 | 适合多线程协作 |
CompletableFuture |
异步任务编排 | 适合链式异步任务、组合任务、有返回值任务 |
面试回答可以这样说
如果 A 线程启动 B 线程后,需要等待 B 线程执行完再继续,可以使用 Thread.join()。A 线程调用 bThread.join() 后会阻塞,直到 B 线程执行结束。也可以使用 CountDownLatch,把计数器初始化为 1,A 调用 await() 等待,B 执行完后调用 countDown(),计数器归零后 A 继续执行。如果是异步编程场景,也可以使用 CompletableFuture,比如 CompletableFuture.runAsync() 启动异步任务,然后调用 join() 等待任务完成。
重点区别
你可以重点记住这句话:
join 是等一个具体线程结束;
CountDownLatch 是等一个或多个任务完成;
CompletableFuture 是异步任务编排工具,可以等待任务完成,也可以处理返回值和后续任务。
另外注意:
sleep 不会释放锁;
wait 会释放锁;
join 本质上是当前线程等待目标线程结束;
CountDownLatch 的 await 会阻塞当前线程;
CompletableFuture 的 join 会阻塞当前线程等待异步任务完成。
七、一句话区别wait和await
wait 是 Object 的方法,配合 synchronized 使用;
await 是并发工具类的方法,常见于 CountDownLatch、Condition 等。
1. wait 是什么?
wait() 是 Object 类的方法。
也就是说,任何对象都可以调用:
lock.wait();
但它必须在 synchronized 代码块或同步方法中使用。
例如:
synchronized (lock) {
lock.wait();
}
否则会抛异常:
IllegalMonitorStateException
2. wait 的特点
调用 wait() 后:
当前线程会释放锁
当前线程进入 WAITING 状态
需要其他线程调用 notify() 或 notifyAll() 唤醒
例子:
Object lock = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("t1 开始等待");
lock.wait();
System.out.println("t1 被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("t2 唤醒 t1");
lock.notify();
}
});
t1 调用 wait() 后会释放 lock,然后等待 t2 调用 notify()。
3. await 是什么?
await() 不是 Object 的方法,它通常出现在 JUC 并发工具类中。
常见的有两类:
CountDownLatch.await();
Condition.await();
CyclicBarrier.await();
它们虽然都叫 await,但含义不完全一样。
4. CountDownLatch.await()
你前面代码里的 await() 是这个:
latch.await();
它的作用是:
当前线程等待,直到 CountDownLatch 的计数器变成 0。
例如:
CountDownLatch latch = new CountDownLatch(1);
Thread b = new Thread(() -> {
System.out.println("B 线程执行任务");
latch.countDown();
});
b.start();
latch.await();
System.out.println("A 线程继续执行");
流程是:
A 调用 latch.await()
A 阻塞等待
B 执行完任务
B 调用 latch.countDown()
计数器从 1 变成 0
A 被唤醒,继续执行
这里的重点是:
CountDownLatch.await() 等的是计数器归零。
它不需要配合 synchronized 使用。
5. Condition.await()
还有一种 await() 是 Condition 的方法。
它和 wait() 比较像。
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
它必须在获取 Lock 之后调用。
对应关系可以这样看:
| synchronized 体系 | Lock 体系 |
|---|---|
synchronized |
ReentrantLock |
wait() |
Condition.await() |
notify() |
Condition.signal() |
notifyAll() |
Condition.signalAll() |
所以:
lock.wait();
对应的是:
condition.await();
而不是 CountDownLatch.await()。
6. 核心区别
| 对比点 | wait() |
CountDownLatch.await() |
Condition.await() |
|---|---|---|---|
| 所属类 | Object |
CountDownLatch |
Condition |
| 使用前提 | 必须在 synchronized 中 |
不需要 synchronized |
必须先获取 Lock |
| 是否释放锁 | 会释放 synchronized 锁 |
不涉及锁释放 | 会释放 Lock |
| 唤醒方式 | notify() / notifyAll() |
countDown() 使计数器归零 |
signal() / signalAll() |
| 主要用途 | 线程通信 | 等待多个任务完成 | Lock 体系下的线程通信 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)