玩转多线程--入门

news/2025/1/11 6:30:32/

目录

什么是多线程?

概念:

优点:

线程和进程区别:(面试常考题)

Java线程和操作系统线程的关系:

多线程创建

方法1继承Thread类

方法2实现Runnable接口

star()和run()的区别:(经典面试题)

其他形变

匿名内部类创建Thread子类对象

匿名内部类创建Runnable子类对象

lambda表达式创建Runnable子类对象

Thread类及其常见方法

Thread的常见构造方法:

Thread常见属性

启动一个线程--start()

中断线程

使用共享标记来中断

调用interrupt()方法来通知

总结:

等待线程--join()

获取当前线程的引用

休眠当前线程

线程的状态

线程安全(重点)

线程不安全:

造成线程不安全的原因:

线程调度是随机的

多个线程修改了同一变量

无法保证原子性

无法保证可见性

观于JMM面试题:

为什么要整这么多内存?

为啥要这么麻烦地拷来拷去?

指令重排序


什么是多线程?

概念:

一个线程就是一个执行流,每个线程之间都可以按照自己的顺序执行自己的代码,多个线程之间“同时”执行多份代码。多线程编程其实也可以称作为“并发编程”。

并发编程:对于进程也可以实现并发编程,但是和线程相比,线程更轻量

  • 创建线程比创建进程更快

  • 销毁线程比销毁进程更快

  • 调度线程比调度进程更快

优点:

  • 多线程可以充分利用CPU资源去处理一些复杂业务,从而提升业务的效率

  • 一定程度上可以提高程序处理任务效率,创建线程的个数,根据CPU逻辑处理器的数量作为参考

    • 线程个数<逻辑处理器个数:会提升程序处理任务效率

    • 线程个数>逻辑处理器个数:由于过多的线程,导致有较多线程处于阻塞状态,并且线程创建和销毁也会一定程度加重系统开销,可能会降低程序处理任务效率

线程和进程区别:(面试常考题)

  • 进程包含线程,每一个进程至少有一个线程,即主线程

  • 进程和进程之间不共享内存空间,同一个进程之间的线程共享内存空间

  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位

  • 一个进程挂了一般不会影响其它进程,但是一个线程挂了,可能导致整个进程的崩溃

Java线程和操作系统线程的关系:

  • 线程是操作系统中的概念。操作系统内核实现了线程这样的机制,并且对用户层提供了一些API供用户使用

  • Java标准库中的Thread类可以视为是对操作系统提供的API进一步的抽象和封装

多线程创建

  • Java中线程参与调度执行的步骤:Java中创建一个线程对象->JVM调用系统的API->创建系统中的线程->最终参与CPU调度

  • 线程的执行顺序并没有什么规律,这和CPU的调度有关,由于CPU的调度是“抢占式”执行的,所以哪个线程当前占用CPU资源是不确定的

方法1继承Thread类

  • Thread用来描述一个线程,创建的每一个线程都是Thread的对象

  • 继承Thread类,直接使用this就表示当前对象的引用

 class MyThread extends Thread{//必须要重写Thread类中的run()方法,run()内可以更具业务需求,进行调整@Overridepublic void run() {System.out.println("Mythread");}}public class Test1 {public static void main(String[] args) {//创建MyThread的实例MyThread t1 = new MyThread();//调用start()启动线程,线程真正开始运行t1.start();}}

方法2实现Runnable接口

  • 其中只有一个run()方法,面对多个线程时和Thread类相比方法更方便,多个线程执行同一个任务就使用Runnable()接口

  • 实现Runnable接口,this表示的是MyRunnable()的引用,需要使用Thread.currentThread()

 class MyRunnable implements Runnable{@Override//重写run()方法public void run() {System.out.println("MyRunnable");}}public class Test2 {public static void main(String[] args) {//创建Thread实例,调用Thread构造方法时将Runnable对象作为参数传入Thread t1 = new Thread(new MyRunnable());//根据线程需要,传入对应参数//调用start启动线程t1.start();}}

star()和run()的区别:(经典面试题)

  • start()真实申请系统线程的PCB,从而启动一个线程,参与CPU调度

  • run()定义线程的时候指定线程要执行的任务,如果直接调用,就是Java一个对象中普通的方法

其他形变

匿名内部类创建Thread子类对象
Thread t1 = new Thread(){@Overridepublic void run() {System.out.println("匿名内部类创建线程");}};
匿名内部类创建Runnable子类对象
 Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("匿名内部类创建线程");}});
lambda表达式创建Runnable子类对象
  • 最推荐的编码方式

  • Runnable接口是一个函数式接口,可以通过Lambda表达式创建,本质上就是实现了Runnable接口

  • 函数接口:接口中只定义了一个方法 

Thread t3 = new Thread(()->{System.out.println("Lambda表达式创建线程");});

Thread类及其常见方法

  • Thread类是JVM用来管理线程的一个类,每一个线程都有唯一的Thread对象与之相关联

  • JVM会将这些Thread对象组织起来,用于线程调度,线程管理

Thread的常见构造方法:

方法说明
Thread()创建线程对象
Thread(Runnable target)使用Runnable对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用Runnable对象创建线程对象,并命名

Thread常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  • ID:线程的唯一标识,不同线程不会重复,JVM默认为Thread对象生成的一个编号,是Java层面的,与PCB区分开

    • Thread是Java中的一个类:创建的Thread对象->调用start方法->JVM调用系统API生成一个PCB->PCB与Thread对象一一对应

  • 名称:线程的名称

  • 状态:表示线程所处的情况

  • 优先级高的线程理论上来说更容易被调度到

  • 关于后台线程:JVM会在一个进程的所有非后台进程结束后,才会停止运行,前台线程可以阻止线程的退出

  • 是否存活,可以简单理解为run()方法是否执行结束

启动一个线程--start()

  • 覆写run()方法仅仅是提供了线程的任务清单

  • 调用start方法,才真正申请系统线程PCB,从而启动一个线程,参与CPU调度

中断线程

  • 线程执行到一半需要停止,通过一个信号使线程退出

  • 方案:

    • 通过共享标记来进行沟通

    • 调用interrupt()方法来通知

使用共享标记来中断
public class Test {//设置标志位,变量用volatile修饰,保证内存可见性,后续再线程安全解决会提到public static volatile boolean isQuit = false;public static void main(String[] args) {//线程中断演示Thread t1 = new Thread(()->{while(!isQuit){System.out.println(Thread.currentThread().getName()+"正常工作,没有被中断!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName()+"被中断,停止任务进行!");});System.out.println(Thread.currentThread().getName()+"发出中断指令!");isQuit = true;System.out.println(Thread.currentThread().getName()+"结束发出中断命令!");t1.start();}}
调用interrupt()方法来通知
 public class Test2 {public static void main(String[] args) throws InterruptedException {//线程中断演示Thread t1 = new Thread(() -> {while (!Thread.interrupted()) {System.out.println(Thread.currentThread().getName() + "正常工作,没有被中断!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName() + "被中断,停止任务进行!");});//启动线程t1.start();//线程休眠Thread.sleep(10000);//发出中断信号t1.interrupt();}}

注意 :

  • 如果线程处于运行状态,直接中断线程,不会报异常,符合程序预期

  • 如果线程处于等待状态,就会报一个中断异常

  • 下图所示第一张为上述代码所报异常

  • 修改catch处理逻辑后代码 

 

 

总结:
  • 调用interrupt()方法来通知,如果线程因为调用wait/join/sleep等方法而阻塞挂起,则以InerruptedException异常的形式通知

  • 出现异常时候,要不要结束线程取决于catch中代码的写法,可以选择忽略这个异常,也可以跳出循环过程

等待线程--join()

  • 等待一个线程执行完毕

方法说明
public void join()等待线程到结束
public void join(long millis)最多等待millis毫秒
public void join(long millis,int nanos)更高精度

获取当前线程的引用

 public static void main(String[] args) {Thread t1 = Thread.currentThread();//获取当前线程对象的引用System.out.println(t1.getName());}//一般可以连起来使用Thread.currentThread()+方法Thread.currentThread().getName()

休眠当前线程

  • 因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的

public static void main(String[] args) throws InterruptedException {//获取当前毫秒System.out.println(System.currentTimeMillis());Thread.sleep(3*1000);System.out.println(System.currentTimeMillis());
}

线程的状态

  • 线程的状态是一个枚举类型Thread.State

public static void main(String[] args) {for (Thread.State state:Thread.State.values()) {System.out.println(state);}
}

面试题:共六种线程状态

  • NEW:安排了工作,但是还未执行,创建好了一个线程对象,没有调用start()方法之前是不会创建PCB的

  • RUNNABLE:可工作的,包含正在工作中和即将开始工作->运行+就绪,此时系统中有很多PCB

  • BLOCKED:等待锁的状态,阻塞的一种

  • WAITING:没有等待时间,处于一直死等的状态

  • TIMED_WATING:设置等待时间的等待状态,过时不候

  • TERMINATED:线程执行完成,PCB已经销毁,但是Java对象还在

注意:对于isAlive()方法,可以认为处于不是NEW和TERMINATED状态的都是活着的

线程安全(重点)

线程不安全:

  • 如果多线程状态下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说明这个程序是线程安全的

造成线程不安全的原因:

线程调度是随机的
  • 这是线程安全的罪魁祸首

  • 随机调度使一个线程在多线程环境下,执行顺序存在很多变数

  • 代码编写人员必须保证在任意执行顺序之下,代码都能正常运行

多个线程修改了同一变量
  • 多个线程修改不同变量,不会出现线程安全问题,一个线程修改一个变量,也不会出现线程安全问题

无法保证原子性
  • 原子性:要么全部执行,要么全部不执行

    • 例如:count++这个操作,对应几条CPU指令

      • LOAD:从内存或者寄存器中读取count的值

      • ADD:执行自增

      • STORE:把计算结果写回寄存器或内存

    • 如果能保证原子性,当执行count++代码的时候,上述三条指令连续执行,不会被打断

  • 无法保证原子性可能带来的问题:可能会发生覆盖现象

    • 如果一个线程正在对一个变量操作,这时中途插入其他线程,这个操作会被打断,结果就可能产生覆盖

      • 这点也和线程抢占式调度密切相关,如果线程不是抢占式的,就算没有原子性,也问题不大

  • 一条Java语句不一定使原子的,也不一定使一条指令

无法保证可见性
  • 可见性:一个线程对共享变量值的修改,能够及时地被其它线程看到,可以通过某种方式,让线程之间相互通信

  • Java内存模型(JMM):Java虚拟机规范中定义了Java内存模型

    • 线程之间的共享变量存储在主内存中

    • 每一个线程都有自己的"工作内存"

    • 当线程要读取一个共享变量的时候,会先把变量从主内存中读取到自己的工作内存中,再从工作内存中读取数据

    • 当线程要修改一个共享变量的时候,会先修改工作内存中的副本,再同步回主内存

观于JMM面试题:
  • 所有线程不可以直接修改内存中的变量

  • 如果要修改,需要把这个变量从主内存中复制到自己的工作内存中

  • 各个线程之间无法相互通信,做到了内存级别的线程隔离

为什么要整这么多内存?
  • 实际上没有这么内存,这只是Java规范中的一个术语,是属于”抽象“的叫法

  • 所谓”主内存“才是真正硬件角度的”内存“,而所谓”工作内存“,则是指CPU的寄存器和高速缓存

为啥要这么麻烦地拷来拷去?
  • 因为CPU访问自身寄存器速度以及高速缓存速度,远远超过访问内存地速度(快了3-4个数量级,也就是几千倍,上万倍)

  • 那访问寄存器这么好,要啥内存?--因为太贵

    • 价格排序:CPU寄存器>内存>硬盘

    • 访问速度:CPU寄存器>内存>硬盘

指令重排序
  • 由于一条Java语句可能对应多条机器指令

  • 我们写的代码在编译之后可能会与代码对应的指令顺序不同,这个过程就是指令的重排序

  • 编译器对于指令重排序的前提是”保持逻辑不发生变化“,这一点在单线程下比较容易判断,但是在多线程环境下就没有那么容易,多线程代码执行复杂程度高,编译器很难在编译阶段对代码的执行效果进行预测,因此激进的重排序很容易导致优化后的逻辑和之前不等价

xdm多线程需要解决的内容非常多,一篇文章不足已掌握,关于线程安全的解决请听下回分解!!


http://www.ppmy.cn/news/1562172.html

相关文章

常用字符串处理函数

常用字符串处理函数 strcspn函数原型参数说明返回值使用示例注意事项 strpbrk函数原型参数说明返回值使用示例 strcasecmp函数原型参数说明返回值使用示例注意事项 strcspn strcspn 是一个 C 和 C 标准库函数&#xff0c;用于计算一个字符串中不包含任何指定字符的最长前缀的长…

rk3568 , buildroot , qt ,使用sqlite, 动态库, 静态库

问题说明&#xff1a; 客户反馈 &#xff0c;buildroot 系统 &#xff0c;使用qt 使用sqlite &#xff0c;有报错&#xff0c;无法使用sqlite. 测试情况说明&#xff1a; 我自己测试&#xff0c;发现&#xff0c; buildroot 自己默认就是 使能了 sqlite 的。 是否解决说明&…

Java 和 C++ 的性能对比分析

&#x1f496; 欢迎来到我的博客&#xff01; 非常高兴能在这里与您相遇。在这里&#xff0c;您不仅能获得有趣的技术分享&#xff0c;还能感受到轻松愉快的氛围。无论您是编程新手&#xff0c;还是资深开发者&#xff0c;都能在这里找到属于您的知识宝藏&#xff0c;学习和成长…

webrtc自适应分辨率的设置

DegradationPreference 是一个枚举类&#xff0c;用于在视频编码或实时通信&#xff08;如 WebRTC&#xff09;中指定系统资源不足时如何处理质量下降的策略。以下是该枚举类的中文解释&#xff1a; enum class DegradationPreference {// 禁用&#xff1a;不根据资源过载信号…

标定 3

标定场景与对应的方式 标定板标定主要应用场景: (1)无法获取到执行机构物理坐标值,比如相机固定,执行机构为传送带等 (2)相机存在畸变等非线性标定情况,需要进行畸变校正 (3)标定单像素精度 (4)获取两个相机之间的坐标系关系 标定板操作步骤: (1)确定好拍…

【踩坑记录❌】ubuntu 安装 NVIDIA 显卡驱动不要 autoinstall

背景 在 ubuntu 22.04 安装 NVIDIA 显卡驱动参考了 博客 的步骤进行&#xff0c;发现有很多评论也出现了无法联网的情况 后续解决 尝试了网卡驱动下载的各类方法&#xff0c;安装驱动的过程中又缺失内核头、 gcc 编译器等文件。由于没有网络&#xff0c;每次缺失的文件只能从…

基于Express+vue+高德地图API实现的出行可视化APP

基于Expressvue高德地图API实现的出行可视化APP 1.项目简介 解决出行问题&#xff0c;用于出行行程记录&#xff0c;路线规划&#xff0c;数据可视化分析的移动端webapp 1.1技术栈 前端&#xff1a;移动端&#xff0c;vue全家桶&#xff0c;Mand组件库&#xff0c;Echarts.…

数据结构——栈的实现

今天&#xff0c;我们来写一下关于栈的博文。 1.首先我们先了解一下什么是栈&#xff1f; 一&#xff1a;概念&#xff1a; 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。 进行数据插入和删除操作的一端称为栈顶&#xff0c;另…