10分钟巩固多线程基础

news2024/10/6 4:05:49

10分钟巩固多线程基础

前言

多线程是并发编程的基础,本篇文章就来聊聊多线程

我们先聊聊概念,比如进程与线程,串行、并行与并发

再去聊聊线程的状态、优先级、同步、通信、终止等知识

进程与线程

什么是进程?

操作系统将资源分配给进程,使用进程进行调度,但进程遇到阻塞任务时,为了提升CPU利用率,会进行切换进程

由于切换进程的成本太高,线程就诞生了

线程又被称为轻量级进程(LWP),线程是操作系统的基本调度单位,当线程被分到CPU给的时间片时就能够进行调度任务

当线程等待资源遇到阻塞时,为了提升CPU利用率会将线程进行挂起,等到后续资源准备好了又将线程恢复,分配到时间片后继续执行

为了安全起见,线程分为用户态和内核态,使用线程操作普通的任务时处于用户态就可以调度执行,要完成某些有关操作系统安全性相关的操作时,需要先切换到内核态再进行操作

线程的挂起、恢复就需要在用户态与内核态中进行切换,频繁的切换线程也会带来一定的开销

当我们点击打开浏览器时,浏览器程序可能会启动一个或多个进程

一个进程下有一个或多个线程,进程用于管理操作系统所分配的资源,线程用于进行调度,并且同一进程下所有线程能共享进程的资源,而线程中为了存储调度的任务运行情况,也会有自己私有的内存空间对其进行存储

用户态与内核态的线程模型实现分为三种:用户线程与内核线程一对一、多对一和多对多

一对一模型

一对一模型实现简单,一个用户线程映射一个内核线程,Java中采用的模型就是一对一

image.png

但如果线程使用不当,可能导致频繁切换内核态,带来大量开销

并且内核线程资源是有限的,因此一对一模型中线程资源有上限

多对一

在多对一模型中

image.png

由于多个用户线程映射同一内核线程,相比于一对一模型能够使用的用户线程更多

但是当发生阻塞时要切换到内核态进行阻塞,该内核线程对应的所有用户线程都会被阻塞,其实现也会变复杂

多对多

在多对多模型中

image.png

不仅解决一对一模型线程上限问题,还解决多对一模型中内核线程阻塞对应所有用户线程都阻塞的问题

但实现变得更加复杂

串行、并行与并发

为什么要用多线程?

随着硬件的发展,多数机器已经不在是单个核心CPU的机器,大量的机器都使用多核超线程技术

串行可以理解成排队执行,当线程分到CPU的资源时开始执行调度,线程可能进行IO任务的调度

此时会等待IO资源准备好才能进行调度,这段时间内CPU啥事也没干从而没有有效的利用CPU

image.png

为了提高CPU的利用率,在A线程等待IO资源时,可以将A线程先挂起,将CPU的资源分配给B线程

当A线程等待的IO资源准备好时,再将B线程挂起恢复A线程继续执行

两个线程在一段时间内看上去像在同时执行,实际上它们是交替执行,某个时刻上只有一个线程在执行

并发提升CPU的利用率,但也会带来线程上下文切换的开销

image.png

那什么又是并行呢?

上面说的串行、并发都在单线程下可以实现,但是并行的前提就是多核

并行指的是多个线程在某个时刻上也是同时执行,因此需要多核

image.png

那是不是多线程一定效率最快呢?

经过上面的分析,我们知道:线程挂起和恢复,上下文的切换会经过用户态、内核态的转换,会有性能开销

当线程太多、运行时频繁进行上下文切换,那么带来的性能开销甚至可能超过并发提升CPU利用率带来的收益

创建线程

JDK中为我们提供的线程类是java.lang.Thread,它实现Runnable接口,用构造接受Runnable的实现

  public class Thread implements Runnable {
      private Runnable target;
  }

Runnable接口是函数式接口,其中只有run方法,run方法中的实现表示该线程启动后要去执行的任务

  public interface Runnable {
      public abstract void run();
  }

Java中创建线程的方式只有一种:创建Thread对象,再去调用start方法,启动线程

我们可以通过构造器创建线程的同时设置线程的名称,并设置要实现的任务(打印线程名称 + hello)

      public void test(){
          Thread a = new Thread(() -> {
              //线程A hello
              System.out.println(Thread.currentThread().getName() + " hello");
          }, "线程A");
          //main hello
          a.run();
          a.start();
      }

当主线程中调用run方法时,实际上是主线程去执行runnable接口的任务

前文我们说过,Java中的线程模型是一对一模型,一个线程对应一个内核线程

只有调用start方法时,才去调用本地方法(C++方法),启动线程执行任务

image.png

如果调用两次start则会抛出IllegalThreadStateException异常

线程状态

Java中的Thread的状态分为新建、运行、阻塞、等待、超时等待、终止

 public enum State {
     //新建
     NEW,
     //运行
     RUNNABLE,
     //阻塞
     BLOCKED,
     //等待
     WAITING,
     //超时等待
     TIMED_WAITING,
     //终止
     TERMINATED;
 }

在操作系统中将运行分为就绪、运行中状态,当线程创建好后等待CPU分配时间片的状态就是就绪状态,分配到时间片运行就是运行中状态

image.png

新建:线程刚创建和还未获取到CPU分配的时间片

运行:线程获取到CPU分配的时间片,进行任务调度

阻塞:线程调度过程中,因无法获取共享资源导致进入阻塞状态(比如被synchronized阻塞)

等待:线程调度过程中,执行wait、join等方法进入等待状态,等待其他线程唤醒

超时等待:线程调度过程中,执行sleep(1)、wait(1)、join(1)等设置等待时间的方法时进入超时等待状态

终止:线程执行完调度任务或者异常执行进入终止状态

优先级

线程需要调度任务的前提是获取CPU资源(CPU分配的时间片)

在Java中提供setPriority方法来设置获取CPU资源的优先级,范围是1~10,默认为5

  //最小
  public final static int MIN_PRIORITY = 1;

  //默认
  public final static int NORM_PRIORITY = 5;

  //最大
  public final static int MAX_PRIORITY = 10;

但设置的优先级只是Java层面的,映射到操作系统的优先级又是不同的

比如在Java设置优先级5或6,可能映射到操作系统的优先级处于同一级别

守护线程

什么是守护线程?

可以把守护线程理解成后台线程,当程序中所有非守护线程执行完任务时,程序会结束

简而言之,无论守护线程是否执行完,只要非守护线程执行完,程序就会结束

因此守护线程可以用来做一些检查资源的后台操作

使用setDaemon(true)方法让线程变成守护线程

线程同步

当多线程需要使用共享资源时,由于共享资源数量有限,它们不能同时获取

每时刻只能有一个线程获取,其他未获取到共享资源的线程就需要被阻塞

如果多线程同时使用共享资源可能会造成逻辑错误

在Java中常用synchronized关键字使用加锁的方式来保证同步(只有一个线程能够访问共享资源)

         synchronized (object){
             System.out.println(object);
         }

其中object就是加锁的共享资源

对于更多synchronized的描述可以查看这篇文章:15000字、6个代码案例、5个原理图让你彻底搞懂Synchronized

线程通信

等待wait / 通知 notify

使用synchronized时要去获取锁,获取锁后线程才能执行调度,当调度中不满足执行条件时,需要让出锁让其他线程执行

比如生产者/消费者模型,当生产者获取到锁要进行生产资源时,发现资源已经满了,它应该让出锁,等到消费者消费完时将它唤醒

这种等待/通知模式是实现线程通信的一种方式,Java提供wait、notify方法来实现等待/通知模式

使用wait、notify的前提是获取到锁

wait让当前线程释放锁进入等待模式,等待其他线程使用notify唤醒

wait(1)也可以携带等待的时间ms,当时间到达时自动唤醒,并开始竞争锁

notify 唤醒等待当前锁的某个线程

notifyAll 唤醒所有等待当前锁的线程

其具体实现可以查看15000字、6个代码案例、5个原理图让你彻底搞懂Synchronized 的锁升级中重量级锁那一小节

生产者消费者模型

生产者、消费者模型中常用等待与通知进行线程通信

生产者检查到生产的资源已满时就进入等待,等待消费者消费完来唤醒,生产完再去唤醒消费者

消费者检查到没有资源时就进入等待,等待生产者生产完来唤醒,消费完再去唤醒生产者

生产

public void produce(int num) throws InterruptedException {
    synchronized (LOCK) {
        //如果生产 资源 已满 等待消费者消费
        while (queue.size() == 10) {
            System.out.println("队列满了,生产者等待");
            LOCK.wait();
        }

        Message message = new Message(num);
        System.out.println(Thread.currentThread().getName() + "生产了" + message);
        queue.add(message);
        //唤醒 所有线程
        LOCK.notifyAll();
    }
}

消费

public void consume() throws InterruptedException {
    synchronized (LOCK) {
        //如果队列为空 等待生产者生产
        while (queue.isEmpty()) {
            System.out.println("队列空了,消费者等待");
            LOCK.wait();
        }
        Message message = queue.poll();
        System.out.println(Thread.currentThread().getName() + "消费了" + message);
        //唤醒 所有线程
        LOCK.notifyAll();
    }
}
sleep 睡眠

sleep 方法用于让线程睡眠一段时间ms

与wait的区别是sleep睡眠时不会释放锁、并且使用sleep时不需要先获取锁

join 等待

join方法用于等待某个线程执行完

比如,在主线程上调用thread.join()就需要等待thread线程执行完,join方法才会返回

同时join也支持设置等待时间ms,超时自动返回

终止线程

终止线程一般使用安全的终止方式:中断线程

线程运行时会保存一个标记位,默认为false,表示没有其他线程对其进行中断

当想要某个线程停止时,可以对其进行中断,比如线程A.interrupt(): 对线程A执行中断操作 ,此时线程A的中断标识为true

当线程调度任务期间,轮询到中断标识为true时就会停止,可以使用线程A.isInterrupted(): 查看线程A的中断标记

当线程进入等待状态时,被其他线程中断会发生中断异常,会清楚标志位并抛出中断异常;可以在catch块中捕获处理进行清理资源或资源的释放

当在根据中断标识循环执行时,还可以自己中断自己停止继续执行

         Thread thread = new Thread(() -> {
             //中断标识为false就循环执行任务
             while (!Thread.currentThread().isInterrupted()) {
                 try {
                     //执行任务
                     System.out.println(" ");

                     //假设等待资源
                     TimeUnit.SECONDS.sleep(1);

                     //获得资源后执行

                 } catch (InterruptedException e) {
                     //等待时中断线程会在抛出异常前恢复标志位
                     //捕获异常时,重新中断标志(自己中断)
                     Thread.currentThread().interrupt();

                     //结束前处理其他资源
                 }
             }
             // true
             System.out.println(" 中断标识位:" + Thread.currentThread().isInterrupted());
         });

还有一种检测中断的方式Thread.interrupted(): 查看当前线程的中断标记,并清除当前线程的中断标记,中断标记恢复为false

最后(不要白嫖,一键三连求求拉~)

本篇文章被收入专栏 由点到线,由线到面,深入浅出构建Java并发编程知识体系,感兴趣的同学可以持续关注喔

本篇文章笔记以及案例被收入 gitee-StudyJava、 github-StudyJava 感兴趣的同学可以stat下持续关注喔~

案例地址:

Gitee-JavaConcurrentProgramming/src/main/java/A_Thread

Github-JavaConcurrentProgramming/src/main/java/A_Thread

有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

关注菜菜,分享更多干货,公众号:菜菜的后端私房菜

本文由博客一文多发平台 OpenWrite 发布!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1048931.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

外汇天眼:假投资平台COINPAYEX非法吸金2158万,项目小组循线破获诈团据点

近年来加密货币投资愈来愈普及,许多人们开始关注并投入这个市场,期望能够获得高额报酬,甚至达到财务自由的目标。 但需要注意的是,市面上充斥各种黑平台,一不小心恐怕蒙受极大的损失。 9/12,警政署刑事警察…

c语言 - 实现每隔1秒向文件中写入当前系统时间

实现思路 主要是通过库函数和结构体获取当前系统时间(年月日和时分秒)保存到变量里,然后通过格式化输出函数将当前系统时间输出到文件中去。 但是需要注意的是题目要求每隔 1 s对系统时间进行输出,所以需要加入 sleep()函数进行调…

windows 环境变量设置

方法一&#xff1a;打开环境变量设置窗口&#xff08;或者cmd 执行sysdm.cpl命令&#xff09; 方法二&#xff1a; powershell执行如下命令&#xff0c;注意修改<要增加的环境变量值>值为要设置的环境变量值 [Environment]::SetEnvironmentVariable("PATH", $E…

2023最新团购社群小程序源码 社区商城小程序源码开源版 带完整后台+部署教程

随着互联网的不断发展&#xff0c;社群团购成为了一种新型的商业模式。在这种模式下&#xff0c;通过社群分享和团购折扣&#xff0c;消费者可以享受到更优惠的价格和更便捷的购物体验。给大家分享一个2023最新团购社群小程序源码&#xff0c;包含社区商城小程序源码开源版和完…

openGauss学习笔记-83 openGauss 数据库管理-内存优化表MOT管理-内存表特性-MOT使用内存和存储规划

文章目录 openGauss学习笔记-83 openGauss 数据库管理-内存优化表MOT管理-内存表特性-MOT使用内存和存储规划83.1 MOT内存规划83.2 存储IO83.3 容量需求 openGauss学习笔记-83 openGauss 数据库管理-内存优化表MOT管理-内存表特性-MOT使用内存和存储规划 本节描述了为满足特定…

No132.精选前端面试题,享受每天的挑战和学习

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…

基于python的智慧城市社区服务平台及养老服务子系统的设计与实现

文章目录 导文摘要前言绪论课题背景国内外现状与趋势国内现状:国外现状:趋势:课题内容相关技术与方法介绍系统分析系统设计系统实现导文 基于python的智慧城市社区服务平台及养老服务子系统的设计与实现 摘要 本文基于Python开发了一款智慧城市社区服务平台及养老服务子系统…

ant-design-pro的可编辑表格editprotable的遇到的一些小问题

需求&#xff1a;1、使用可编辑表格实现对某一行表格的数据进行编辑&#xff0c;对输入内容控制必须低于同行某一数据 2、解决搜索数据和列名展示不同&#xff0c;和不可编辑的列不展示在搜索框 实现效果 解决搜索和readonly之间的问题 因为本次需求只要求修改一个数据&…

ElementPlus· tab切换/标签切换 + 分页

tab切换 ---> <el-tabs><el-tab-pane>... 分页 --------> <el-pagination> tab切换 // tab标签切换 // v-model双向绑定选项中的name&#xff0c;tab-change事件在 activeName改变时触发 <script setup> const tabChange (tab, event)>{…

【JAVA】飞机大战

代码和图片放在这个地址了&#xff1a; https://gitee.com/r77683962/fighting/tree/master 最新的代码运行&#xff0c;可以有两架飞机&#xff0c;分别通过WASD&#xff08;方向&#xff09;&#xff0c;F&#xff08;发子弹&#xff09;&#xff1b;上下左右&#xff08;控…

大型企业网如何部署NAT实现需求

1.企业中堕胎电脑如何共享上网&#xff1f; 2.NAT地址转换原理讲解&#xff1b; 3.企业机房如何用NAT让服务器更安全&#xff1f; - NAT - 网络地址转换 - 什么式网络地址 IP地址 -通信时候的设备标识 - 为什么要把IP地址做转换呢&#xff1f; -- 公网IP&#xff…

迷你无人车 Navigation 导航(5)— 基础框架学习

迷你无人车 Navigation 导航&#xff08;5&#xff09;— 基础框架学习 整个功能包整个功能包集合以move_base为核心&#xff0c;将里程计信息、传感器信息、定位信息、地图以及目标点输入给move_base&#xff0c;move_base经过规划后会输出速度指令 move_base包括三个关键部分…

如何像人类一样写HTML之第一个HTML、标签的关系与注释

文章目录 前言一、创建第一个HTML文档创建方式1创建方式2 二、标签的关系三、注释的使用总结 前言 在现代数字化社会中&#xff0c;网页是我们获取信息、与他人交流以及展示自己的重要工具之一。要了解如何创建网页&#xff0c;HTML&#xff08;Hypertext Markup Language&…

给网站引入各大搜索引擎的关键字提示,白给的接口薅他羊毛

属于是导航网站必要的了 因为网站是个导航网站&#xff0c;有一个搜索框&#xff0c;用户搜索时需要像百度一样有个搜索提示关键词列表。 之前只是用百度&#xff0c;现在给增加了好几个&#xff0c;想换那个就换那个了&#xff0c;直接上图片 可以配置属于是想选哪个选哪个&…

树莓派4B使用Docker部署SpringBoot项目——(二)使用docker安装mysql

树莓派4B使用docker安装mysql 使用的树莓派4B为8GB版本&#xff0c;使用docker pull命令拉取MySQL镜像&#xff1a; docker pull --platformarm64 mysql/mysql-server等待拉取完成 使用docker images查看镜像 创建MySQL容器 docker run -di --namemysql -p 3306:3306 -e MY…

(vue3)create-vue 组合式API

优势&#xff1a; 更易维护&#xff1a;组合式api&#xff0c;更好的TS支持 之前是选项式api&#xff0c;现在是组合式&#xff0c;把同功能的api集合式管理 复用功能封装成一整个函数 更快的速度 更小的体积 更优的数据响应式&#xff1a;Proxy create-vue 新的脚手架工…

批量剪辑视频的秘密武器,添加SRT字幕并提升你的视频品质,高效剪辑的艺术

在如今的数字时代&#xff0c;视频内容的制作和分享变得越来越普遍。如果你是一个视频创作者&#xff0c;或者经常需要编辑和分享视频内容&#xff0c;那么我们为你带来了一款视频批量剪辑工具&#xff0c;让你轻松在固定位置添加字幕&#xff0c;打造专业级剪辑效果&#xff0…

五金零售批发商城小程序的作用是什么

五金产品在人们生活中不可或缺&#xff0c;无论需求高低&#xff0c;总归会购买&#xff0c;而每个城市的五金店也非常多&#xff0c;市场高应用度也促进了各个品牌商们增长&#xff0c;而在实际销售中&#xff0c;无论厂家还是门店商家都会面临一些痛点&#xff0c;如品牌宣传…

Spine Web Player教程

官方文档教程&#xff1a; Spine Web Player 报错&#xff1a; 1、Q:报Animation bounds are invalid XX错误&#xff1f; A:请校对cdn或者npm install的版本号是否与json资源内版本号一致。

SQL做流水号

SELECT REPLICATE(0, 3 - LEN(3)) 3 AS 流水号 SELECT REPLICATE(0, 5 - LEN(3)) 3 AS 流水号 SELECT REPLICATE(0, 8 - LEN(3)) 3 AS 流水号