【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 类
classMyThreadextendsThread{@Overridepublicvoidrun(){System.out.println("线程执行");}}publicclassDemo{publicstaticvoidmain(String[]args){newMyThread().start();}}特点:
简单直接,但是 Java 是单继承,继承 Thread 后就不能继承其他类。方式二:实现 Runnable 接口
classMyRunnableimplementsRunnable{@Overridepublicvoidrun(){System.out.println("线程执行");}}publicclassDemo{publicstaticvoidmain(String[]args){Threadt=newThread(newMyRunnable());t.start();}}特点:
更推荐,任务和线程分离,避免单继承限制。也可以用 Lambda:
newThread(()->{System.out.println("线程执行");}).start();方式三:实现 Callable 接口结合 FutureTask
Runnable没有返回值,也不能直接抛出受检异常。
如果任务需要返回结果,可以使用Callable。
importjava.util.concurrent.Callable;importjava.util.concurrent.FutureTask;publicclassDemo{publicstaticvoidmain(String[]args)throwsException{Callable<Integer>callable=()->{return100;};FutureTask<Integer>futureTask=newFutureTask<>(callable);Threadt=newThread(futureTask);t.start();Integerresult=futureTask.get();System.out.println(result);}}特点:
适合需要返回值的异步任务。 futureTask.get() 会阻塞当前线程,直到任务执行完并返回结果。方式四:通过线程池创建线程
importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;publicclassDemo{publicstaticvoidmain(String[]args){ExecutorServicepool=Executors.newFixedThreadPool(3);pool.execute(()->{System.out.println("线程池执行任务");});pool.shutdown();}}特点:
实际开发中更推荐使用线程池。因为线程池可以:
复用线程 减少频繁创建和销毁线程的开销 统一管理线程资源 提高系统稳定性四、多线程的应用场景
多线程适合处理一些耗时任务,避免主线程被长时间阻塞。
比如:
文件拷贝 文件上传下载 加载大量资源 网络请求 聊天软件收发消息 后台服务器处理多个请求 定时任务 日志异步写入 消息队列消费举个例子:
如果一个服务器只有一个线程,那么同时来了 100 个请求,只能一个一个处理。
使用多线程后,可以多个请求同时被处理,提高并发能力。
五、A 线程启动 B 线程,A 要等 B 执行完再继续,怎么实现?
这个问题的核心是:
线程之间的协作 / 等待另一个线程执行完成常见方法有三种:
Thread.join()CountDownLatchCompletableFuture方法一:Thread.join()
代码
publicclassJoinDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadbThread=newThread(()->{System.out.println("B 线程开始执行");try{Thread.sleep(2000);}catch(InterruptedExceptione){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可以理解为一个倒计时计数器。
代码
importjava.util.concurrent.CountDownLatch;publicclassCountDownLatchDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{CountDownLatchlatch=newCountDownLatch(1);ThreadbThread=newThread(()->{System.out.println("B 线程开始执行");try{Thread.sleep(2000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("B 线程执行完毕");latch.countDown();});bThread.start();latch.await();System.out.println("A 线程继续执行");}}原理
CountDownLatchlatch=newCountDownLatch(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 三个线程都执行完:
CountDownLatchlatch=newCountDownLatch(3);每个线程执行完:
latch.countDown();A 线程:
latch.await();只有计数器变成 0,A 才继续。
方法三:CompletableFuture
CompletableFuture是 Java 8 引入的异步编程工具。
代码
importjava.util.concurrent.CompletableFuture;publicclassCompletableFutureDemo{publicstaticvoidmain(String[]args){System.out.println("A 线程启动 B 线程");CompletableFuture.runAsync(()->{System.out.println("B 线程执行");try{Thread.sleep(2000);}catch(InterruptedExceptione){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(); }否则会抛异常:
IllegalMonitorStateException2. 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 体系下的线程通信 |
