线程
- 进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
- 线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
一、线程调度
- 各个线程轮流获得CPU的使用权,分别执行各自的任务。
那么,在可运行池中,会有多个线程处于就绪状态等到CPU,JVM就负责了线程的调度,JVM采用的是抢占式调度,因此可能造成多线程执行结果的随机性。
备注:
(1)一个CPU在任意时刻只能执行一条计算机指令,每个线程只有获得CPU的使用权才能执行指令
(2)抢占式调度:指的是每条线程执行的时间、线程的切换都由系统控制。
二、线程的状态及转换

- Java线程的六种状态
| 状态名称 | 说明 |
| NEW | 初始状态, 线程刚被构建,但是还没调用start()方法 |
| RUNNABLE | 运行状态,Java系统中将操作系统中的就绪和运行两种状态笼统地称为“运行中” |
| BLOCKED | 阻塞状态,表示线程阻塞于锁 |
| WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程做出一些特定动作(通知或者中断) |
| TIME_WAITING | 超时等待状态,该状态不同于等待状态,它可以在指定的时间后自行返回 |
| TERMINATED | 中止状态,表示该线程已经执行完毕 |
- 备注:Java线程只有六种状态,网上还有说五种状态,其实那个操作系统的五种状态。
- wait()与sleep()和notify()理解:
wait()方法针对的是一个被同步代码块加锁的对象,在进入WAITING状态时会释放对象的锁,直到被notify()或notifyAll()唤醒线程,被唤醒的线程便会进入该对象的锁池中,锁池中的线程会去竞争该锁对象。
sleep()方法针对的是一个线程,执行sleep()会进入TIME_WAITING状态,仅仅让你的线程进入睡眠状态,而不会释放对象的锁。
三、开启线程的方式
- 开启线程共三种方式
1.继承Thread类创建线程类
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表线程要完成的任务。
(2)创建Thread子类的实例。
(3)调用线程对象的start()方法来启动线程。
****示例代码:****
//第一步:定义Thread类的子类
public class TestThread extends Thread{
//重写run方法,run()方法称为执行体。
public void run(){
System.out.println("执行方法体")
}
public static void main(String[] args){
//第二步、第三步:创建Thread子类的实例并执行start()来启动线程
new TestThread().start();
}
}
2.通过Runnable接口创建线程类
(1)定义Runnable接口的实现类,并重写该接口的run方法。
(2)创建Runable实现类的实例,并依此实例作为Thread的参数来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。
****示例代码:****
//第一步:定义Rnnable接口的实现类,并重写该接口的run方法
public class TestRunnable implement Runnable{
public void run(){
System.out.println("执行Runnable执行体")
}
public void main(String[] args){
//第二步上半节:创建Runnable实现类的实例
TestRunnable tr = new TestRunnable();
//第二步下半节、第三步:依据创建的Runnable实例作为参数传进Thread对象,并执行其start()方法
new Thread(tr,"线程1")。start();
}
}
3.通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutrueTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的参数创建并启动新线程。
(4)调用FutrueTask对象的get()方法来获得子线程执行结束后的返回值。
****示例代码:****
//第一步:实现Callable接口,并实现call()方法
public class CallableTest implement Callable<Integer>{
//call()方法作为线程执行体,有返回值
@Override
public Integer call() throw Exception{
int i = 100;
return i;
}
public static void main(String[] args){
//第二步上半节:创建Callable实现类的实例
CallableTest ct = new CallableTest();
//第二步下半节:使用FutrueTask类来包装Callable对象
FutureTask<Integer> ft = new FutureTask<>(ct);
//第三步:调用FutureTask对象作为Thread对象的参数创建并启动线程
new Thread(ft).start();
try{
//第四步:调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
System.out.println("子线程返回值:"+ft.get());
}catch(ExecutionException e){
e.printStackTrace();
}
}
}
备注:
- FutureTask类实际上是同时实现了Runnable和Future接口,由此才使得其具有Future和Runnable双重特性。
通过Runnable特性,可以作为Thread对象的target,而Future特性,使得其可以取得新创建线程中的call()方法的返回值。
public class FutureTask<V> implements RunnableFuture<V> {
//....
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
四、线程关闭
- 个人认为有两种方式:
1.使用标志位
很简单的设置一个标志位,启动线程后,定期检查里这个标志位。如果检测到这个标志位为某个值,线程马上结束。
****示例代码:****
public class MyThread implements Runnable{
//设置标志位,需要为volatile,保证线程读取时isCancelled是最新数据。
private volatitle boolean isCancelled;
public void run(){
//定期检查这个标志位,如果检测到某个值,线程马上结束
//但并不完善。考虑下,如果线程执行的方法被阻塞,那么如何执行isCancelled的检查呢?线程有可能永远不会去检查标志位,也就卡住了。
while(!isCancelled){
//doSomething
}
}
public void cancel(){
isCancelled = true;
}
}
2.使用中断
每个线程都有个boolean类型的中断状态。当使用Thread的interrupt()方法时,线程的中断状态会被设置为true。
对线程调用interrupt()方法,不会真正中断正在运行的线程,只是发出一个请求,由线程在合适时候结束自己。
Thread.currentThread().interrupt();
备注:
- Java提供了中断机制,Thread类下有三个重要方法。
(1)public void interrupt()
(2)public boolean isInterrupted()
(3)public static boolean interrupted(); // 清除中断标志,并返回原状态
五、线程阻塞的原因
- 导致线程阻塞一般有一下原因:
| 原因 | 说明 |
| 线程进行了休眠 | 线程执行了Thread.sleep(int n)方法,线程放弃CPU,睡眠n毫秒,然后恢复运行 |
| 线程执行wait()进入阻塞状态 | 线程执行了一个对象的wait()方法,进入阻塞状态,只要等到其他线程执行了该对象的notify或notifyAll()方法,才能将其唤醒。 |
| 等待相关资源 | 线程执行I/O操作或进行远程通信时,会因为等待相关资源而进入阻塞状态。 |
| 线程等待获取同步锁才能进行下一步操作 | 线程要执行一段同步代码,由于无法获得相关的同步锁,只好进入阻塞状态,等到获取了同步锁,才能恢复运行。 |
| 请求连接时 | 请求与服务器建立连接时,即当线程执行Socket的带参数的构造方法,或执行Socket的connect()方法时,会进入阻塞状态,直到连接成功,此线程才从Socket的构造方法或Connect()方法返回。 |
备注:
1)线程同步和线程协作的理解
1-1)线程同步(Synchronization):确保多个线程对共享资源的访问是互斥且有序的,防止数据竞争(Data Race)导致的数据不一致或程序崩溃。
典型场景:
- 多个线程同时修改一个共享计数器。
- 多个线程读写同一个文件或数据库连接。
1-2)线程协作(Cooperation):让多个线程按照特定顺序协同工作,例如一个线程等待另一个线程完成任务后再继续执行。
典型场景:
- 生产者-消费者模型:生产者生成数据,消费者消费数据,缓冲区空或满时线程等待。
- 多阶段任务:多个线程分阶段执行任务,每个阶段需要所有线程完成后才能进入下一阶段。
2)Runnable和completableFulture的区别?
2-1)定义与核心目标
| 特性 | Runnable | CompletableFuture |
| 本质 | 一个接口,定义线程任务 | 一个类,表示异步计算的结果 |
| 核心目标 | 描述一个可运行的任务 | 处理异步操作的结果,支持链式调用和组合任务 |
| 返回值 |
(无返回值) | 支持泛型返回值(如 ) |
| 异常处理 | 无法抛出受检异常(只能内部处理) | 支持显式异常处理(如 ) |
2-2)使用场景
Runnable
- 简单异步任务:不需要返回结果,只关注任务执行本身。
- 线程池提交:通过
ExecutorService.submit(Runnable)提交任务。 - 示例:日志记录、发送通知、定时任务等。
Runnable task = () -> {
System.out.println("Task executed by: " + Thread.currentThread().getName());
};
new Thread(task).start();
CompletableFuture
- 复杂异步流程:需要组合多个异步操作,处理结果依赖或异常。
- 链式调用:通过
thenApply(),thenCompose(),thenCombine()等组合任务。 - 示例:调用多个服务后聚合结果、异步任务依赖处理等。
CompletableFuture.supplyAsync(() -> fetchDataFromAPI())
.thenApply(data -> processData(data))
.thenAccept(result -> saveResult(result))
.exceptionally(ex -> {
System.err.println("Error: " + ex.getMessage());
return null;
});
更多java基础总结(适合于java基础学习、java面试常规题):
总结篇(9)---字符串及基本类 (1)字符串及基本类之基本数据类型
总结篇(10)---字符串及基本类 (2)字符串及基本类之java中公共方法及操作
总结篇(12)---字符串及基本类 (4)Integer对象
总结篇(14)---JVM(java虚拟机) (1)JVM虚拟机概括
总结篇(15)---JVM(java虚拟机) (2)类加载器
总结篇(16)---JVM(java虚拟机) (3)运行时数据区
总结篇(17)---JVM(java虚拟机) (4)垃圾回收
总结篇(18)---JVM(java虚拟机) (5)垃圾回收算法
总结篇(19)---JVM(java虚拟机) (6)JVM调优
总结篇(24)---Java线程及其相关(2)多线程及其问题
总结篇(25)---Java线程及其相关(3)线程池及其问题
总结篇(26)---Java线程及其相关(4)ThreadLocal
总结篇(27)---Java并发及锁(1)Synchronized
总结篇(31)---JUC工具类(1)CountDownLatch
本文围绕Java线程展开,介绍了线程调度,JVM采用抢占式调度;阐述了线程的六种状态及wait()、sleep()等方法的区别;说明了开启线程的三种方式,包括继承Thread类、实现Runnable接口等;还提及线程关闭的两种方式及线程阻塞的原因。

被折叠的 条评论
为什么被折叠?



