玩转多线程--入门

server/2025/1/11 9:05:02/

目录

什么是多线程?

概念:

优点:

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

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/server/157421.html

相关文章

标准应用 | 2025年网络安全服务成本度量实施参考

01 网络安全服务成本度量依据相关新变化 为了解决我国网络安全服务产业发展中面临的服务供需两方对于服务成本组成认知偏差较大、网络安全服务成本度量缺乏依据的问题&#xff0c;中国网络安全产业联盟&#xff08;CCIA&#xff09;组织北京赛西科技发展有限责任公司、北京安…

python异常机制

异常是什么&#xff1f; 软件程序在运行过程中&#xff0c;非常可能遇到刚刚提到的这些问题&#xff0c;我们称之为异常&#xff0c;英文是Exception&#xff0c;意思是例外。遇到这些例外情况&#xff0c;或者交异常&#xff0c;我们怎么让写的程序做出合理的处理&#xff0c…

【SQL】Delete使用

目录 语法 需求 示例 分析 代码 语法 DELETE删除表中所需内容 删除表中满足特点条件的行&#xff1a;DELETE FROM 表名 WHERE 条件; 删除表中所有行&#xff1a;DELETE FROM 表名; WHERE子句 WHERE子句用于指定从表中选取记录的条件。允许筛选数据&#xff0c;只返回满足…

[IoT]详细设计:智能农业监控系统

以下是基于IoT的智能农业监控系统网络拓扑的详细设计&#xff1a; 网络拓扑详细设计 1. 星型与网状混合拓扑 中心节点&#xff1a;本地服务器或集中控制器作为中心节点&#xff0c;负责协调和管理整个网络。传感器/执行器节点&#xff1a;分布在农田中&#xff0c;负责数据采…

STL——二叉搜索树

目录 二叉搜索树的概念 ⼆叉搜索树的性能分析 ⼆叉搜索树的插⼊ ⼆叉搜索树的查找 ⼆叉搜索树的删除 中序遍历结果为升序序列 二叉搜索树的概念 ⼆叉搜索树⼜称⼆叉排序树&#xff0c;它或者是⼀棵空树&#xff0c;或者是具有以下性质的⼆叉树 • 若它的左⼦树不为空&#…

云原生周刊:K8s 生态系统的五大趋势预测

开源项目推荐 Burrito Burrito 是一款 TACoS&#xff08;Terraform Automation and Collaboration Software&#xff09;Kubernetes Operator&#xff0c;旨在提供类似 Argo CD 的体验&#xff0c;用于管理和自动化 Terraform 工作流。通过 Burrito&#xff0c;用户可以在 Ku…

C#Halcon找线封装

利用CreateMetrologyModel封装找线工具时&#xff0c;在后期实际应用调试时容易把检测极性搞混乱&#xff0c;造成检测偏差&#xff0c;基于此&#xff0c;此Demo增加画线后检测极性的指引&#xff0c;首先看一下效果 加载测试图片 画线 确定后指引效果 找线效果 修改显示 UI代…

详细分析 创建并上传到 GitHub 仓库

目录 前言1. 从零创建并上传代码到 GitHub2. 将现有的本地仓库推送到 GitHub 前言 &#x1f91f; 找工作&#xff0c;来万码优才&#xff1a;&#x1f449; #小程序://万码优才/r6rqmzDaXpYkJZF 创建仓库的时候&#xff0c;平台已经有所提供流程&#xff01; 1. 从零创建并上传…