「超级详细」Java线程实现原理_java线程用法
moboyou 2025-08-26 02:15 9 浏览
本章内容
基本概念
程序、进程和线程
程序、进程、线程之间的关系:
- 程序指的是以文件形式存储在磁盘上的计算机指令的集合。
- 进程指的是一个执行中的程序,每一个进程拥有独立的内存空间和系统资源。
- 线程是CPU调度的最小单元,它运行在进程中,不能独立存在。
系统线程模型
操作系统中关键概念:
- 内核线程KLT:内核级线程(Kemel-Level Threads)直接由操作系统内核支持,线程创建、销毁、切换开销较大。
- 用户线程UT:用户线程(User Thread)建立在用户空间,系统内核不能感知用户线程的存在,线程创建、销毁、切换开销小。
- 轻量级进程LWP:轻量级进程(Light weight process)是由操作系统提供给用户操作内核线程接口的实现,是用户级线程和内核级线程之间的中间层。
系统线程模型主要有三种:
- 内核线程模型。
- 用户线程模型。
- 混合线程模型。
内核线程模型
内核线程模型依赖操作系统内核提供的内核线程(Kernel-Level Thread ,KLT)来实现多线程。在此模型下,线程的切换调度由系统内核完成,系统内核负责将多个线程执行的任务映射到各个CPU中去执行。
程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口:轻量级进程(Light Weight Process,LWP),轻量级进程即通常意义上的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型。
如图所示:
其中:
- Process:进程。
- LWP:轻量级进程(即:线程)。
- KLT:内存线程。
对于Sun JDK来说,它的Windows版与Linux版都是使用一对一的线程模型实现的,一个Java线程映射一个轻量级进程之中,因为Windows和Linux系统提供的是一对一的线程模型。
用户线程模型
用户线程模型中多个用户线程对应一个内核线程(KLT)。从广义上来讲,一个线程只要不是内核线程,就可以认为是用户线程(User Thread),轻量级进程也属于用户线程,但轻量级进程的实现建立在内核之上,许多操作都要进行系统调用,效率会受到限制。
使用用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要用户程序自己处理(如:线程的创建、切换、调度等)。
如图所示:
其中:
- Process:进程。
- UT:用户线程。
- KLT:内存线程。
混合线程模型
线程除了依赖内核线程实现和完全由用户程序自己实现之外,还有一种将内核线程与用户线程一起使用的实现方式。在这种混合实现下,既存在用户线程,也存在轻量级进程。
用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。
在这种混合模式中,用户线程与轻量级进程的数量比是不定的,即为N:M的关系。
如图所示:
其中:
- UT:用户线程。
- LWP:轻量级进程(即:线程)。
- KLT:内存线程。
线程调度策略
线程调度指的是系统为线程分配处理器使用权的过程。
线程调度方式有两种:
- 协同式线程调度(Cooperative Threads-Scheduling):线程调度由其本身来控制,线程在自身工作执行完成后,主动通知系统切换到另一个线程执行。
- 抢占式线程调度(Preemptive Threads-Scheduling)线程的调度由系统分配执行时间,线程的切换由系统决定。
JAVA使用的是抢占式线程调度策略。
线程生命周期
线程生命周期有五种状态:初始状态、可运行状态、运行状态、休眠状态和终止状态。
如图所示:
其中:
- 初始状态:该状态下线程已经被创建,但是还不允许分配CPU执行。该状态属于编程语言特有状态。注意:此处的线程已经被创建指的是在编程语言层面被创建,在操作系统层面的线程还没有被创建。
- 可运行状态:该状态下线程可以分配CPU执行。在这种状态下,真正的操作系统线程已经被创建,可以分配CPU执行。
- 运行状态:当有空闲的CPU时,操作系统会将空闲的CPU分配给一个处于可运行状态的线程,被分配到CPU的线程的状态会由可运行状态转换成运行状态。
- 休眠状态:运行状态的线程如果调用一个阻塞的API(如:以阻塞方式读文件)或者等待某个事件(如:条件变量),则线程的状态会由运行状态转换成休眠状态,同时释放CPU使用权,休眠状态的线程永远没有机会获得CPU使用权。当等待的事件出现,线程就会从休眠状态转换成可运行状态。
- 终止状态:线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期已结束。
以上五种状态在不同编程语言中会进行简化合并。如:
- C语言的POSIX Threads规范,就将初始状态和可运行状态进行了合并。
- Java语言中将可运行状态和运行状态进行了合并,可运行状态和运行状态在操作系统调度层面有用,而在JVM层面无需关心这两个状态,因为JVM将线程调度交给操作系统进行处理。
除了简化合并,以上五种状态也有可能被细化,如:Java语言中细化了休眠状态。
Java线程
Java线程分类
Java线程一般分为两类:
- 用户线程:主要负责业务逻辑的处理和控制。
- 守护线程:主要支持用户线程的工作。只有在用户线程存在的情况下才能存在,JVM会在所有的用户线程结束后自动退出。
Java线程生命周期
Java线程有六种状态:
- NEW(初始化):线程创建后还未调用start()方法启动线程。
- RUNNABLE(可运行):线程调用start()方法启动线程,等待CPU调度。RUNNABLE分为RUNNING(运行中)和READY(就绪)状态。处在RUNNING状态的线程可以调用yield()方法让出CPU使用权,并与其他处于READY状态的线程一起等待被调度。
- WAITING(等待):处于RUNNABLE状态的线程调用wait()方法后,线程就处于等待状态,需要其他线程显示地唤醒。
- TIMED_WAITING(超时等待):处于RUNNABLE状态的线程调用wait(long)方法后,线程就处于等待状态,无需其他线程显示地唤醒,等待指定时间后由系统自动唤醒。
- BLOCKED(阻塞):等待进入synchronized()方法或代码块,处于阻塞状态。
- TERMINATED(终止):线程已经执行结束。
如图所示:
其中,BLOCKED、WAITING、TIMED_WAITING可以理解为线程导致休眠状态的三种原因。
状态转换
1)RUNNABLE与BLOCKED的状态转换
只有一种场景会触发这种转换,就是线程等待synchronized隐式锁。synchronized修饰的代码块、方法同一时刻只能有一个线程执行,其他线程只能等待,这种情况下,等待的线程就会从RUNNABLE转换到BLOCKED状态。 而当等待的线程获取到了synchronized隐式锁时,又会从BLOCKED转换到RUNNABLE状态。
2)RUNNABLE与WAITING的状态转换
有三种场景会触发RUNNABLE到WAITING的状态转换:
- 获得synchronized隐式锁的线程,调用无参数的Object.wait()方法。
- 调用无参数的Thread.join()方法。
- 调用LockSupport.park()方法。其中LockSupport对象是Java并发包中的锁,调用LockSupport.park()方法,当前线程会阻塞,线程的状态会从RUNNABLE转换到WAITING。调用LockSupport.unpark(Thread thread)可唤醒目标线程,目标线程的状态又会从WAITING状态转换到RUNNABLE。
3)RUNNABLE与TIMED_WAITING的状态转换
有五种场景会触发RUNNABLE到TIMED_WAITING的状态转换:
- 调用带超时参数的Thread.sleep(long millis)方法。
- 获得synchronized隐式锁的线程,调用带超时参数的Object.wait(long timeout)方法。
- 调用带超时参数的Thread.join(long millis)方法。
- 调用带超时参数的LockSupport.parkNanos(Object blocker, long deadline)方法。
- 调用带超时参数的LockSupport.parkUntil(long deadline)方法。
4)从NEW到RUNNABLE状态
调用线程对象的start()方法。
5)从RUNNABLE到TERMINATED状态
线程执行完run()方法后,会自动转换到TERMINATED状态,如果执行run()方法时抛出异常,也会导致线程终止。如果需要强制中断run()方法的执行,可调用interrupt()方法。
Java多线程实现
Java多线程实现方式有五种:
- 继承Thread类。
- 实现Runnable接口。
- 实现Callable接口。
- JDK8以上版本使用CompletableFuture进行异步调用。
- 使用线程池。
继承Thread类
@Slf4j
public class ThreadImpl extends Thread{
@Override
public void run() {
log.info("继承Thread类");
}
}实现Runnable接口
@Slf4j
public class RunnableImpl implements Runnable{
@Override
public void run() {
log.info("实现Runnable接口");
}
}实现Callable接口
@Slf4j
public class CallableImpl implements Callable {
@Override
public Object call() throws Exception {
return "实现Callable接口";
}
@SneakyThrows
public static void main(String[] args) {
FutureTask task = new FutureTask(new CallableImpl());
Thread thread = new Thread(task);
thread.start();
log.info(String.valueOf(task.get()));
}
}使用CompletableFuture进行异步调用
@Slf4j
public class CompletableFutureImpl {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(2);
CompletableFuture<String> futureTask = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
log.info("任务开始");
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
return "执行异常";
}
log.info("任务结束");
return "执行成功";
}
}, threadPool);
// 异步获取FutureTask执行结果
futureTask.thenAccept(e-> log.info("执行结果:{}",e));
}
}使用线程池
@Slf4j
public class ThreadPoolExecutorImpl {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(
1,
2,
0,
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
for(int i = 0;i < 3;i++){
pool.execute(new ThreadTask());
}
}
static class ThreadTask implements Runnable {
@Override
public void run() {
log.info(Thread.currentThread().getName());
}
}
}Java线程通信
Java线程间的通信方式有以下几种:
- 共享变量:多个线程共享同一个变量,通过对变量的读写操作来实现线程间的通信。可以使用synchronized关键字来保证线程安全。
- wait()和notify():使用Object类的wait()和notify()方法来实现线程间的通信。wait()方法使线程进入等待状态,直到其他线程调用notify()方法唤醒它。notify()方法用于唤醒等待中的线程。
- Condition:使用Condition接口来实现线程间的通信。Condition接口提供了await()和signal()方法,类似于wait()和notify()方法,但更加灵活。
- 管道(Pipe):使用管道来实现线程间的通信。一个线程将数据写入管道,另一个线程从管道中读取数据。
- 阻塞队列(BlockingQueue):使用阻塞队列来实现线程间的通信。一个线程将数据放入队列,另一个线程从队列中取出数据。阻塞队列提供了put()和take()方法,当队列满时,put()方法会阻塞线程,直到队列有空闲位置;当队列空时,take()方法会阻塞线程,直到队列有数据。
- 信号量(Semaphore):使用信号量来实现线程间的通信。信号量可以控制同时访问某个资源的线程数量。
Java线程中断
Java中的线程中断是一种协作机制,调用线程对象的interrupt()方法并不会立即中断正在运行的线程,而是向线程发出中断请求,线程在合适的时机自行中断。
线程中断主要方法
线程中断涉及的主要方法:
- interrupt()方法:该方法为实例方法,在线程内外都可发起调用。主要作用是中断此线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程),实际上只是给线程设置一个中断标识,线程仍会继续运行:
- 作用于正常执行线程时会将中断标记设置为true。
- 作用于阻塞线程时会将中断标志重置为false(中断标识默认初始值为false)。
- interrupted()方法:该方法为Thread类的静态方法,只能在线程内部调用。主要作用是测试当前线程是否被中断(检查中断标识),返回一个boolean并重置中断状态,第二次再调用时中断状态已经被重置,将返回一个false。
- isInterrupted()方法:该方法的主要作用是只检测此线程是否被中断 ,不修改中断状态。
线程中断场景分析
线程中断应用场景:
- 中断处于阻塞状态的线程:
- 当线程A处于WAITING、TIMED_WAITING状态(如:线程调用sleep()、join()、wait()等方法)时,如果其他线程调用线程A的interrupt()方法,会使线程A返回到RUNNABLE状态,同时线程A的代码会触发InterruptedException异常。
- 当线程A处于RUNNABLE状态,并且阻塞在java.nio.channels.InterruptibleChannel上时,如果其他线程调用线程A的interrupt()方法,线程A会触发java.nio.channels.ClosedByInterruptException异常;而阻塞在java.nio.channels.Selector上时,如果其他线程调用线程A的interrupt()方法,线程A的java.nio.channels.Selector会立即返回。
- 中断正常执行的线程:需要在目标线程内部检测中断位,并由用户手动终止,来做出响应。如:中断计算圆周率的线程A,此时需要依赖线程A主动检测中断状态。如果其他线程调用线程A的interrupt()方法,线程A就可以通过isInterrupted()方法,检测是不是自己是否被中断。
注意:
- 1)synchronized和reentrantLock.lock()在获锁的过程中不能被中断,即:如果产生了死锁,则不可能被中断。但是如果调用带超时的方法reentrantLock.tryLock(long timeout, TimeUnit unit),线程在等待时被中断,则会抛出一个InterruptedException异常,这是一个非常有用的特性,因为它允许程序打破死锁。也可以调用reentrantLock.lockInterruptibly()方法,它就相当于一个超时设为无限的tryLock()方法。
- 2)当需要中断线程时,最佳实践就是利用线程的中断位,而不是自定义中断状态,因为当线程被阻塞时,原生中断位仍然会被监听,而自定义的则不能。
线程中断编写范式
while循环在try块中编写范式
@Override
public void run() {
try {
...
/**
* 理论上调用sleep()、join()、wait()等方法时发生异常会退出循环,但可能存在线程调用了阻塞方法而没有进入阻塞的情况,
* 因此,需要判断当前线程是否被中断
*/
while (!Thread.currentThread().isInterrupted()&& more work to do) {
do more work;
}
} catch (InterruptedException e) {
// 线程阻塞期间(如:调用sleep()、join()、wait()等方法)被中断
} finally {
// 线程结束前做善后处理(如:资源清理等)
}
}try块在while循环中编写范式
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()&& more work to do) {
try {
...
sleep(delay);
} catch (InterruptedException e) {
// 重新设置中断标识
Thread.currentThread().interrupt();
}
}
}线程中断代码示例
/**
* @author 南秋同学
* 线程中断代码示例
*/
@Slf4j
public class InterruptExample {
@SneakyThrows
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
// 异常被捕获后,阻塞被打破、中断位被重置为false,因此,此处永远输出false
log.warn("线程是否被中断:{}", Thread.currentThread().isInterrupted());
log.info("回收资源");
e.printStackTrace();
// 注意:这里需要再次将中断位设为true,否则外面的while循环不会退出
Thread.currentThread().interrupt();
log.info("重新设置中断标识:{}", Thread.currentThread().isInterrupted());
}
log.info("子线程逻辑");
}
});
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
log.info("中断子线程:{}", thread.isInterrupted());
TimeUnit.SECONDS.sleep(5);
}
}stop()和interrupt()区别
stop()和interrupt()方法中断线程的主要区别:
- stop()方法会真的杀死线程,如果线程持有ReentrantLock锁,调用stop()方法中断线程不会自动调用ReentrantLock的unlock()方法释放锁,那么其他线程就再也没机会获得ReentrantLock锁。注意:stop()方法已经被标注为@Deprecated。
- interrupt()方法不会立刻杀死线程,它仅仅是通知线程,线程有机会执行一些后续操作,同时也可以忽略这个通知。
【阅读推荐】
更多精彩内容,如:
- Redis系列
- 数据结构与算法系列
- Nacos系列
- MySQL系列
- JVM系列
- Kafka系列
- 并发编程系列
请移步【南秋同学】个人主页进行查阅。内容持续更新中......
【作者简介】
一枚热爱技术和生活的老贝比,专注于Java领域,关注【南秋同学】带你一起学习成长~
相关推荐
- Excel技巧:SHEETSNA函数一键提取所有工作表名称批量生产目录
-
首先介绍一下此函数:SHEETSNAME函数用于获取工作表的名称,有三个可选参数。语法:=SHEETSNAME([参照区域],[结果方向],[工作表范围])(参照区域,可选。给出参照,只返回参照单元格...
- Excel HOUR函数:“小时”提取器_excel+hour函数提取器怎么用
-
一、函数概述HOUR函数是Excel中用于提取时间值小时部分的日期时间函数,返回0(12:00AM)到23(11:00PM)之间的整数。该函数在时间数据分析、考勤统计、日程安排等场景中应用广泛。语...
- Filter+Search信息管理不再难|多条件|模糊查找|Excel函数应用
-
原创版权所有介绍一个信息管理系统,要求可以实现:多条件、模糊查找,手动输入的内容能去空格。先看效果,如下图动画演示这样的一个效果要怎样实现呢?本文所用函数有Filter和Search。先用filter...
- FILTER函数介绍及经典用法12:FILTER+切片器的应用
-
EXCEL函数技巧:FILTER经典用法12。FILTER+切片器制作筛选按钮。FILTER的函数的经典用法12是用FILTER的函数和切片器制作一个筛选按钮。像左边的原始数据,右边想要制作一...
- office办公应用网站推荐_office办公软件大全
-
以下是针对Office办公应用(Word/Excel/PPT等)的免费学习网站推荐,涵盖官方教程、综合平台及垂直领域资源,适合不同学习需求:一、官方权威资源1.微软Office官方培训...
- WPS/Excel职场办公最常用的60个函数大全(含卡片),效率翻倍!
-
办公最常用的60个函数大全:从入门到精通,效率翻倍!在职场中,WPS/Excel几乎是每个人都离不开的工具,而函数则是其灵魂。掌握常用的函数,不仅能大幅提升工作效率,还能让你在数据处理、报表分析、自动...
- 收藏|查找神器Xlookup全集|一篇就够|Excel函数|图解教程
-
原创版权所有全程图解,方便阅读,内容比较多,请先收藏!Xlookup是Vlookup的升级函数,解决了Vlookup的所有缺点,可以完全取代Vlookup,学完本文后你将可以应对所有的查找难题,内容...
- 批量查询快递总耗时?用Excel这个公式,自动计算揽收到签收天数
-
批量查询快递总耗时?用Excel这个公式,自动计算揽收到签收天数在电商运营、物流对账等工作中,经常需要统计快递“揽收到签收”的耗时——比如判断某快递公司是否符合“3天内送达”的服务承...
- Excel函数公式教程(490个实例详解)
-
Excel函数公式教程(490个实例详解)管理层的财务人员为什么那么厉害?就是因为他们精通excel技能!财务人员在日常工作中,经常会用到Excel财务函数公式,比如财务报表分析、工资核算、库存管理等...
- Excel(WPS表格)Tocol函数应用技巧案例解读,建议收藏备用!
-
工作中,经常需要从多个单元格区域中提取唯一值,如体育赛事报名信息中提取唯一的参赛者信息等,此时如果复制粘贴然后去重,效率就会很低。如果能合理利用Tocol函数,将会极大地提高工作效率。一、功能及语法结...
- Excel中的SCAN函数公式,把计算过程理清,你就会了
-
Excel新版本里面,除了出现非常好用的xlookup,Filter公式之外,还更新一批自定义函数,可以像写代码一样写公式其中SCAN函数公式,也非常强大,它是一个循环函数,今天来了解这个函数公式的计...
- Excel(WPS表格)中多列去重就用Tocol+Unique组合函数,简单高效
-
在数据的分析和处理中,“去重”一直是绕不开的话题,如果单列去重,可以使用Unique函数完成,如果多列去重,如下图:从数据信息中可以看到,每位参赛者参加了多项运动,如果想知道去重后的参赛者有多少人,该...
- Excel(WPS表格)函数Groupby,聚合统计,快速提高效率!
-
在前期的内容中,我们讲了很多的统计函数,如Sum系列、Average系列、Count系列、Rank系列等等……但如果用一个函数实现类似数据透视表的功能,就必须用Groupby函数,按指定字段进行聚合汇...
- Excel新版本,IFS函数公式,太强大了!
-
我们举一个工作实例,现在需要计算业务员的奖励数据,右边是公司的奖励标准:在新版本的函数公式出来之前,我们需要使用IF函数公式来解决1、IF函数公式IF函数公式由三个参数组成,IF(判断条件,对的时候返...
- Excel不用函数公式数据透视表,1秒完成多列项目汇总统计
-
如何将这里的多组数据进行汇总统计?每组数据当中一列是不同菜品,另一列就是该菜品的销售数量。如何进行汇总统计得到所有的菜品销售数量的求和、技术、平均、最大、最小值等数据?不用函数公式和数据透视表,一秒就...
- 一周热门
- 最近发表
-
- Excel技巧:SHEETSNA函数一键提取所有工作表名称批量生产目录
- Excel HOUR函数:“小时”提取器_excel+hour函数提取器怎么用
- Filter+Search信息管理不再难|多条件|模糊查找|Excel函数应用
- FILTER函数介绍及经典用法12:FILTER+切片器的应用
- office办公应用网站推荐_office办公软件大全
- WPS/Excel职场办公最常用的60个函数大全(含卡片),效率翻倍!
- 收藏|查找神器Xlookup全集|一篇就够|Excel函数|图解教程
- 批量查询快递总耗时?用Excel这个公式,自动计算揽收到签收天数
- Excel函数公式教程(490个实例详解)
- Excel(WPS表格)Tocol函数应用技巧案例解读,建议收藏备用!
- 标签列表
-
- 外键约束 oracle (36)
- oracle的row number (32)
- 唯一索引 oracle (34)
- oracle in 表变量 (28)
- oracle导出dmp导出 (28)
- 多线程的创建方式 (29)
- 多线程 python (30)
- java多线程并发处理 (32)
- 宏程序代码一览表 (35)
- c++需要学多久 (25)
- css class选择器用法 (25)
- css样式引入 (30)
- css教程文字移动 (33)
- php简单源码 (36)
- php个人中心源码 (25)
- php小说爬取源码 (23)
- 云电脑app源码 (22)
- html画折线图 (24)
- docker好玩的应用 (28)
- linux有没有pe工具 (34)
- 可以上传视频的网站源码 (25)
- 随机函数如何生成小数点数字 (31)
- 随机函数excel公式总和不变30个数据随机 (33)
- 所有excel函数公式大全讲解 (22)
- 有动图演示excel函数公式大全讲解 (32)
