Java——多线程

一.多线程

1.什么是多线程

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程的实际运作单位

简单理解多线程就是应用软件中相互独立,可以同时运行的功能(也可以理解为人体内相互独立,但可以同时运行的器官⌓‿⌓)

我们平时常用的Main方法,就是主线程

2.多线程的作用

单线程运行时,比如我们要创建一个变量,程序是需要等待时间的。而使用多线程,程序可以在多个线程之间来回运行,充分利用等待的时间,从而提高CPU的利用效率

8fbbe4c556f54fc2b81c772714c4b246.png

二.多线程的三种实现方式

在Java API中对于Thread类的描述中给出了多线程的两种启动方式

⒈Thread类

线程是程序中的执行线程。Java虚拟机允许应用程序并发地运行多个执行线程

6bac42e8fcab42da94de354d34f65a8e.png

下面我们就来结合多线程的启动方式来了解Thread类

⒉多线程的第一种启动方式

将类声明为Thread的子类

该子类应重写Thread类的run方法

接下来可以分配并启动该子类的实例

f40dfae5e6e84a0ba6e621d5aa1bad3c.png

如图:我们创建两条线程并启用

66e6069a5bec43e2a5cde66eb6e5b51e.png

3d829dfe3fe24549bb15f753e46cf0e8.png

⑴此方法中的Thread类

①构造方法

Ⅰ.Thread()

Ⅱ. Thread(String name)

其中的参数传递的就是线程的名字,默认为Thread-序号

②常用方法

Ⅰ start

void start() 使该线程开始执行

start方法是Thread类的基础方法,有了它线程才能够启动执行,而它又不像流那样需要close关闭,当线程结束后它会自动关闭

Ⅱ getName

String getName() 返回该线程的名称

当线程我们没有手动命名,getName默认返回的是Thread-序号

Ⅲ sleep

20e30a243aa046d981217b33b552cf64.png

在多条线程执行时,当一条线程抢占到CPU执行权,它的执行时间是不确定的,那么它就可能一直占有着,当执行完毕后才能轮到下一个线程

04a653f4f5634a2582c700036068d055.png

比如上图,当一条线程执行完毕后才能轮到下一个线程

那我们想要线程轮流执行,那么就可以使用sleep让线程睡一会 

如图:当我们执行完打印语句后,就让线程睡1毫秒,将执行权让给另一条线程

7933a4c48f6142459f5b6d3c997eff1b.png

如图:看运行结果,两条线程就差不多是交替执行,而不是一条线程执行到底

6da30569c36245988b3a2e2c1916f814.png

Ⅳ  setPriority

void setPriority(int newPriority)  设置线程的优先级

void getPriority()  获取线程的优先级

在Java中多条线程的执行是随机的,线程的优先级分为10个等级(1--10),优先级高的获取到CPU执行权的概率就越高,Java默认优先级为5

如图:优先级高的抢占到执行权的概率越大,而不是一定是它先执行完,这是概率问题

827b803babf44429bf5e18c3d59b3684.png

Ⅴ  setDaemon

 void setDaemong(boolean on)  将该线程设置为守护线程

守护线程就是当其他非守护线程执行完毕后,守护线程就没有存在的必要了,就会陆续结束,该守护线程可以不执行完结束

如图:我们将线程1设置为守护线程,线程2为非守护线程,当线程2执行完毕时,线程2会陆续结束,可以不会执行完

fdf09fc78a2643be85045c4f0769a119.png

250e7b5bc7e343b6ad23d0336a614871.png 

举个例子,当我们在扣扣的聊天窗口传输数据,聊天窗口就是非守护线程,而传输数据的窗口就是守护线程,当我们把聊天窗口关闭时,数据传输窗口也会陆续关闭

Ⅵ  yield

static void yield()  暂停当前正在执行的线程对象,并执行其他线程

yield方法可以出让当前的CPU执行权,但出让之后该线程仍然可能抢占到CPU执行权

因此除了sleep方法,该方法也可以让线程的执行尽可能的均匀些

Ⅶ  join

f2b63552f4a1488f8f5b91e356be307f.png

join方法设置的线程,当该线程抢占到CPU执行权之后,在规定的等待时间内就会一直执行完毕后,才会让出CPU的执行权

⒊多线程的第二种启用方式

将类声明实现Runable接口

然后实现run方法

最后创建子类对象并传递给Thread对象

4decdde05b2c4a22aebcd606c93dab62.png

如图:我们创建子类对象并传递给线程执行

a810a944cf424e79927e6739d405d4cd.png

4698037a794a4ba186ccf0654a19b97c.png

⑴此方法中的Thread类

①构造方法

92750b45c4514c2dac4f1493afbf62a8.png

方法传递Runable接口的实现类对象 

又因为Runable接口是函数式接口,因此我们可以使用匿名内部类的方式实现

f0a8a9aef1b742a396ca635d27fca751.png

②常用方法

Ⅰ currentThread

static void currentThread()  获取当前线程对象

Ⅱ getName

Runable方法启动线程获取线程名字与Thread方法获取不同

在Runable实现类中我们实现的是Runable接口,该类与Thread是没有关系的,那么我们就不能像第一种启用方式那样直接getName获取到线程的名字了

那怎么解决呢?

我们可以利用currentThread方法获取到当前执行线程的对象,然后再调用getName方法获取到当前线程的名字

02b364d27d264ed59cd2a8fcf7b1d255.png

4.多线程的第三种启用方式

在前面的两种启用方式中,run方法是没有返回值的

因此我们就需要一种有返回值的启用多线程的方法

FutureTask类可以的get方法可以获取到线程方法中的结果,且FutureTast实现了Runable接口,可以在第二种方式的基础上启用线程

f5d69d3771ec4f6c9de394fb9eea7a76.png

而其中构造方法可以传递一个Callable接口的实现类对象

e1d75cc6e0734956a6d1d7e56b3139b4.png

而Callable接口只有一个方法call可以返回线程计算的结果

d0529f7729644a6b9d280ac5abb01419.png

因此第三种启用方式就是在第二种方式的基础上稍加修改

首先创建Callable实现类并重写call方法

然后创建FutureTesk对象接受Callable实现类运行的结果

最后创建Thread类对象并启动

dc17b68f954a49a68d5644e89bea05c9.png

ce295bd6ad95406a96c5dfed03c4d9e1.png 

5.三种线程启用方式的对比

对于第一种启用方式,操作简单,可以直接使用Thread类中的方法。但是正因为它继承了Thread类,它就不能再继承其他类了,因此它的扩展性较差

对于第二,三种启用方式,它没有继承任何类,因此它的扩展性就强些。但是它的编程相对的就复杂些,不能直接使用Thread类中的方法

第一,二种启用方式无法获取到方法返回值,因此就有了第三种方式可以获取到方法返回值

三.线程的安全问题

在多线程的执行中,若我们有一个共享的变量size在随着线程的执行变化着,因为线程的执行是随时随机抢占的,那么size就会有线程安全问题

如图所示例子:

8c2786d8231f4875a63ab56c543f930b.png

那么为了维护线程操作共享数据运行时的安全性问题,我们可以将这共享数据的代码用锁锁起来,当线程进入锁后,其他线程在外等候,当锁里面的代码执行完毕后,其他线程才能抢夺执行

1.同步代码块

格式:

synchronized(锁){操作的共享数据}

这里面的锁对象一定要是唯一的,只有相同的一把锁我们才可以让多条线程开锁解锁

这把锁可以是任意类型的共享对象

如:static Object o=new Object;

但是通常我们会使用本类的字节码文件

类名.class

如图:我们利用本类的字节码文件作为唯一的锁对象

e35f9b93837241ecaa07c29428a09151.png

2.同步方法

同步方法就是把synchronized关键字加到方法上,表示把这一个方法的所有代码给锁起来

格式:

修饰符 synchronized 返回值类型 方法名(方法参数){}

同步方法的锁对象我们不能自己指定,Java给我们指定好了锁对象

当是非静态方法时,锁对象是this,表示方法调用者

当是静态方式时,所对象是当前类的字节码文件

如图:在之前我们学习的StringBuffer中,我们同用可以看到同步方法的身影,这表示StringBuffer是线程安全的

611c00140ee8484c9d559364f08bed48.png

3. Lock锁

synchronized操作简单但是我们无法进行更多关于锁的操作,而Lock相比于synchronized可以进行更广泛的锁定操作,允许更灵活的结构,可以支持多个相关的Condition对象

⑴Lock类

如图: Lock类是一个接口,不能直接实例化,我们常用它的实现类ReentrantLock(可重入锁)来实例化

18ddadbc7b1f4c01be269b6861489f66.png

①成员方法

3f57a98e622946109a68ed3a3c49ac97.png

Ⅰ lock和unlock

lock开锁, unlock解锁,这两个方法是最基本的锁,同样的锁对象必须是唯一的

利用lock锁有一个小细节

如图:若我们直接lock与unlock,就会遇到下面的问题,有线程拿着钥匙跑了,其他线程结束不了!!!

1793469074e04509a2655ab32a7a03f3.png

因此我们需要一个解决办法,无论线程怎样执行,unlock必须执行。那么我们就可以使用try...finally来包裹unlock,让锁必须释放

19d4d7d5058e4b419d6de5ca898c9960.png

Ⅱ newCondition

lock锁通常会与Condition类结合使用来进行一些对于锁的操作

比如在阻塞队列中的使用

https://blog.csdn.net/m0_74808313/article/details/132196171

4.死锁问题

当锁嵌套时,通常会遇到死锁问题

如图:当线程1拿到了A锁等待B锁,而同时线程2拿到了B锁等待A锁,这时就导致了死锁

2cfa7efe2a1847489477efe8114fd5ad.png

因此为了防止死锁问题,我们好尽量减少锁的嵌套

四.多线程协作

多线程协作就是线程之间相互配合,共同完成某项工作

比如:一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者是消费者

1.生产者和消费者

生产者和消费者模式是一个十分经典的多线程协作的模式,又叫做等待唤醒机制,打破了线程的随机机制,让多个线程轮流执行

所谓生产者消费者问题,实际上主要是包含了两类线程:

​ 一类是生产者线程用于生产数据

​ 一类是消费者线程用于消费数据

生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库

生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为

消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

2.等待唤醒机制的实现

⑴仓库,生产者和消费者的逻辑

首先我们需要一个仓库,当生产者生产物件后放入仓库,消费者消费物件拿出仓库

这个仓库需要有一把锁

当生产者进入时,若发现仓库中有物件,那么它就会沉睡等待,若发现仓库中没有物件,那么它就会生产物件并叫醒消费者

765cf1c6968640a4b80ae4ca7d65d170.png

当消费者进入时,若发现仓库中没有物件,那么它就会沉睡等待;若发现仓库中有物件,那么它就会消费物件并叫醒生产者

83c30897c80e4a1cb612d0b54ebcb3d4.png

⑵wait和notify

在Java的Object类中提供了相对应的方法来帮助我们解决线程的协作问题

d4833395a4a74d35903c82e865975ee0.png

d79d5bc661504b5da30f2f7b2d9851c1.png

注: wait和notify必须使用在同步方法或同步代码块内

⑶阻塞队列实现

首先我们需要一个阻塞队列,这个阻塞队列就代表仓库

然后我们完成生产者与消费者的逻辑

因为阻塞队列的put与take方法就是生产者与消费者的逻辑,因此我们在写生产者与消费者时就直接put,take,不用再进行逻辑的实现

af7e1868d9314b83a15b0e6bff146505.png

如图:我们写完生产者与消费者的逻辑,传递阻塞队列查看

37df77784e7440a094ef797112beae20.png

40e3152bb1424072943a435cd102f472.png

fae3ee76baa84c13b095ee98aad70d7a.png 

如图:查看打印语句发现生产者与消费者是轮流执行的,这样就实现了等待唤醒机制的逻辑

8f7769570f344cf7960da4dad72569e7.png

细节:因为锁是在put与take方法内部的,而打印语句在锁的外面,但并不影响共享数据的执行,只是不便于我们查看

五.线程状态

在给定的时间点上,一个线程只能处于一种状态。这些状态是虚拟机状态,它们并没有反映所以操作系统线程状态

73ca8c85b22d4bdbbf312adac17d8828.png

六.线程池

在以前我们写的多线程有弊端

当我们需要线程时就创建(NEW),当它运行完后就消失(TERMINAED),这样的话浪费操作系统的资源

因此我们需要线程池来优化

1.线程池核心原理

我们需要一个容器,当我们提交线程任务时,容器会创建新的线程对象,任务执行完毕,线程存入到容器,到下次直接拿出使用

若提交任务时容器中没有空闲线程且容器满了,那么其他线程排队等待

2.线程池实现

static ExecutorService newCachedThreadPool() 创建一个没有上限的线程池

static ExecutorService newFixedThreadPool(int nThreads) 创建一个有上限的线程池

如图:我们创建一个大小为3的线程池,将前三个任务提交上去

6f7c491f282a4bf7af97f7f8e8e96454.png

可以看到当前排队的线程为0,工作中的线程为3,而当我们要提交下面的任务时,当前面的线程没结束它就会排队等待,只有当前面的线程运行完毕他们才能工作

73f8b0002b5c4970af5c1517a898f162.png

3.自定义线程池

当我们查看newFixedThreadPool时,可以看到它的底层是创建了一个ThreadPoolExecutor对象

b919557680d9410ba9b9797ec938adc8.png

ThreadPoolExecutor才是真正的线程池对象,它相比于前面的线程池来说更加灵活

⑴ThreadPoolExecutor

ThreadPoolExecutor(int corePoolSize ,  int maximumPoolSize ,  long keepAliveTime ,  TimeUnit unit , BlockingQueue<Runnable> workQueue ,  ThreadFactory threadFactory ,  RejectedExecutionHandler handler)  用给定的初始参数创建线程池

其中共有7个参数

参数一:核心线程数

参数二:最大线程数量

参数三:等待的空闲时间

参数四:时间单位

参数五:任务队列

参数六:创建线程工厂

参数七:要执行的任务过多时的解决方案

其中当我们提交的线程多于核心线程,多出的线程会等待

若线程数量超出最大线程数,那么会创建临时线程(最大线程数-核心线程数),让多出的线程工作

若线程数量超出最大线程数+临时线程,那么会对超出的线程进行处理

6afe4488384c49e0b8b4ca54a38d6c78.png

其中的处理方式有四种,分别被定义为内部类

311b241d720b434ea1a5dc9ab1a13212.png

⑵线程池实现

线程池的大小并不是我们随意规定的,而是需要通过公式计算出来的

①CPU密集型运算

当我们的项目中计算多而读取文件少,就要此方式类计算

最大并行数+1

最大并行数与我们电脑CPU的型号相关,因为操作系统不会把所有的线程给同一个软件,因此我们通常利用Java虚拟机来计算最大并行数

408ae63b49d6468f8c21190eaaa12702.png

②I/O密集型运算

当我们的项目计算少,读取数据多,那么就使用此类方式计算

最大并行数×期望CPU利用率×(总时间/CPU计算时间)

③代码书写

90b3cf11352d4889a4da588327d801fd.png

 

 

 

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

代理IP可靠吗?哪里可以找到可靠的代理?

需要代理来访问受限制的网站或改善您的在线隐私&#xff1f;别再犹豫了&#xff01;在这篇博文中&#xff0c;我们将探讨您可以使用的选项&#xff0c;并提供有关在哪里获取代理的指导。 首先&#xff0c;让我们了解什么是代理及其工作原理。代理充当您的设备和互联网之间的中介…

ATFX:美国通胀率平台期,或助力黄金延续涨势

ATFX金属&#xff1a;5月9日19:00至5月10日19:00&#xff0c;COMEX黄金的小时级别出现一波持续24小时的上涨走势&#xff0c;最高触及2385.3美元&#xff0c;累计涨幅2.78%&#xff0c;成为上周最佳的短线交易时机。R阻力线形成后&#xff0c;COMEX黄金进入下降通道&#xff0c…

MySQL从入门到高级 --- 6.函数

文章目录 第六章&#xff1a;6.函数6.1 聚合函数6.2 数学函数6.3 字符串函数6.4 日期函数6.4.1 日期格式 6.5 控制流函数6.5.1 if逻辑判断语句6.5.2 case when语句 6.6 窗口函数6.6.1 序号函数6.6.2 开窗聚合函数6.6.3 分布函数6.6.4 前后函数6.6.5 头尾函数6.6.6 其他函数6.7 …

Golang面向对象编程(二)

文章目录 封装基本介绍封装的实现工厂函数 继承基本介绍继承的实现字段和方法访问细节多继承 封装 基本介绍 基本介绍 封装&#xff08;Encapsulation&#xff09;是面向对象编程&#xff08;OOP&#xff09;中的一种重要概念&#xff0c;封装通过将数据和相关的方法组合在一起…

哪个牌子防水运动蓝牙耳机比较好?别错过四款高分防水耳机锦集!

在现代生活中&#xff0c;运动与音乐的交织早已超越单纯的休闲娱乐范畴&#xff0c;转而成为人们追求身心健康、提升运动表现的重要元素。尤其对于那些热爱户外运动、热衷于水上活动&#xff0c;或是追求无拘无束训练体验的健身爱好者而言&#xff0c;一款性能优越、防水可靠的…

【代码随想录】【动态规划】背包问题 - 完全背包

完全背包 模板&#xff1a;完全背包问题 问题描述 完全背包问题与01背包问题唯一的区别在于&#xff1a; 在01背包中&#xff1a;每个物品只有一个&#xff0c;要么放入背包&#xff0c;要么不放入背包在完全背包中&#xff1a;每个物品有无限多个&#xff0c;可以不放入背…

LSTM计算指示图

掌握网络结构组件构成 输入门、遗忘门、输出门候选记忆细胞记忆细胞隐藏状态ref&#xff1a;6.8. 长短期记忆&#xff08;LSTM&#xff09; — 《动手学深度学习》 文档 (gluon.ai)

内网环境安装使用DBeaver使用第一天

之前一直使用navicat&#xff0c;现在出于某种原因不让使用了&#xff0c;于是上手了这个工具&#xff0c;说实话&#xff0c;真的&#xff0c;但是必须要用。 首先安装的时候&#xff0c;必须要选择MySQL驱动&#xff0c;如果外网直接选择以后就可以下载了&#xff0c;内网需…

# 从浅入深 学习 SpringCloud 微服务架构(十五)

从浅入深 学习 SpringCloud 微服务架构&#xff08;十五&#xff09; 一、SpringCloudStream 的概述 在实际的企业开发中&#xff0c;消息中间件是至关重要的组件之一。消息中间件主要解决应用解耦&#xff0c;异步消息&#xff0c;流量削锋等问题&#xff0c;实现高性能&…

记录:robot_localization传感器数据融合学习

一、参考资料 官方&#xff1a; http://wiki.ros.org/robot_localizationhttp://docs.ros.org/en/noetic/api/robot_localization/html/index.html2015 ROSCon 演讲官方网址&#xff08;youyube上也有这个视频&#xff09; 实践教程 https://kapernikov.com/the-ros-robot_…

Shell的运行原理和Linux的权限

Shell的运行原理 Linux严格意义上说是一个操作系统&#xff0c;我们称之为“核心&#xff08;kernel&#xff09;”&#xff0c;但我们一般用户不能直接使用kernel&#xff0c;而是通过kernel的“外壳程序”&#xff0c;也就是所谓的Shell&#xff0c;来与kernel沟通。 Shell…

深入探究MySQL常用的存储引擎

前言 MySQL是一个广泛使用的开源关系型数据库管理系统&#xff0c;它支持多种存储引擎。存储引擎决定了MySQL数据库如何存储、检索和管理数据。不同的存储引擎具有不同的特点、性能表现和适用场景。选择适合的存储引擎对于优化数据库性能、确保数据完整性和安全性至关重要。本…