JavaEE多线程(2)

在这里插入图片描述

文章目录

  • 1..多线程的安全
    • 1.1出现多线程不安全的原因
    • 1.2解决多线程不安全的⽅法
    • 1.3三种典型死锁场景
    • 1.4如何避免死锁问题
    • 2.线程等待通知机制
    • 2.1等待通知的作用
    • 2.2等待通知的方法——wait
    • 2.3唤醒wait的方法——notify

1…多线程的安全

1.1出现多线程不安全的原因

  1. 线程在系统中是随机调度,抢占式执⾏。
  2. 多个线程同时修改同⼀个变量
  3. 线程对变量的修改操作不是“原⼦”的
  4. 内存可⻅性问题
  5. 指令重排序、
    “原⼦”是什么?
    原⼦是指不可再拆分的最⼩单位,放到代码操作中就是⼀段代码对应⼀个cpu指令就是原⼦的,如果对应到多个cpu指令就不是原⼦的。

1.2解决多线程不安全的⽅法

从原因⼊⼿:

  1. 原因1由于是系统本⾝的原因⼈为⽆法⼲预。
  2. 原因2是⼀个解决多线程不安全的⽅法,但是只是在特定的场景下才可以实现。
  3. 原因3是解决多线程不安全最合适的⽅法,既然修改的这个操作不是⼀个原⼦,那我们只需要将这
    个不是“原⼦”的操作打包成⼀个原⼦的操作即可。
    引申出⼀个新的词“锁”
    我们可以通过锁来将之前不是原⼦操作打包成⼀个原⼦操作
    关于这个锁本⾝就是系统内核中的api,只不过是jvm将这个api封装了,为了让java可以更好的使⽤这
    个锁。
    关于锁主要操作的两个⽅⾯:
    1.加锁
    ⽐如对t1线程进⾏加锁,t2线程也要加锁,那么t2线程就会阻塞等待(互斥锁/竞争锁/锁冲突)
    2.解锁
    t1线程解锁之后,t2线程才可以进⾏加锁
    注意:此处的锁主要针对于锁对象,对于t1和t2线程是指的同⼀个锁对象,不是同⼀个锁对象那就没
    意义了。
    对锁的总结:
    1.两个操作⸺加锁,解锁
    2.锁的特性⸺互斥
    3.只有多个线程竞争同⼀把锁才会互斥,如果多个线程竞争不同的锁则不会产⽣互斥
    java中⽤⼀个关键字来描述锁⸺synchronized
    1.synchronized 怎么读?怎么拼写?
    synchronized
    2.synchronized()括号中写的是锁对象⸺锁对象可以是任意类实列出的对象
    注意:
    锁对象的⽤途只有⼀个,两个线程是否针对⼀个对象加锁
    如果是就会出现锁竞争/锁冲突/互斥,就会引起阻塞等待
    如果不是就不会出现锁竞争/锁冲突/互斥,就不会引起阻塞等待
    和对象具体是什么类型,和它内部有什么属性,有什么⽅法,接下来是否要操作这个对象,统统都没
    有关系
    3.synchronized下⾯跟着{}
    当进⼊到这个代码块就是对上述(锁对象)进⾏了加锁操作
    当出了代码块就是对上述(锁对象)进⾏了解锁操作
    以下代码就是两个线程针对同⼀个对象locker加锁,当t1线程加锁之后,t2想加锁只能阻塞等待,只有
    等到t1线程解锁之后,t2线程才有可能加锁成功
public static int count = 0;
//创建一个对象作为一个锁对象
public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(()-> {for (int i = 1; i <= 5000; i++) {synchronized(locker) {count++;}}});Thread t2 = new Thread(()-> {for (int i = 1; i <=5000 ; i++) {synchronized(locker) {count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = "+ count);
}

以下代码就是两个线程对不同锁对象进行加锁,就不会产生锁冲突/锁竞争/互斥。

public static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {for (int i = 1; i <=5000 ; i++) {synchronized(locker1) {count++;}}});Thread t2 = new Thread(() -> {for (int i = 1; i <=5000 ; i++) {synchronized(locker2) {count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + count);
}

当t1线程解锁之后,不一定是t2线程拿到锁,有可能是其他线程拿到锁。
join与锁的区别:
join是一个线程完了,再让第二个线程执行。
锁只是针对加锁的那一块代码,就像上述代码中加锁的count++就会变成串行执行,但剩余的代码还是并发执行。
注意:加锁不是针对线程,而是针对共享资源的访问操作,比如现在我对t1线程中的操作1进行了加锁,但是系统内核将t1线程调度走了,可以让其他线程调度到t1线程的位置继续执行操作1,此时t2线程还是无法加到锁.
另外加锁的方式:
1.写一个方法将加锁的关键字放在方法中:

public static int count = 0;
synchronized public void add() {count++;
}
public static void main(String[] args) throws InterruptedException {Deom14 deom14 = new Deom14();Thread t1 = new Thread(()-> {for (int i = 1; i <= 5000; i++) {deom14.add();}});Thread t2 = new Thread(()-> {for (int i = 1; i <=5000 ; i++) {deom14.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = "+ count);
}

对于这种方式加锁还有一种写法:

public static int count = 0;public void add() {synchronized (this){count++;}
}
public static void main(String[] args) throws InterruptedException {Deom14 deom14 = new Deom14();Thread t1 = new Thread(()-> {for (int i = 1; i <= 5000; i++) {deom14.add();}});Thread t2 = new Thread(()-> {for (int i = 1; i <=5000 ; i++) {deom14.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = "+ count);
}

利用this,谁调用了这个方法就用这个对象作为锁对象,
2.synchronized对static方法进行加锁,相当于对类的类对象进行加锁

public static int count = 0;
synchronized static void func() {count++;
}public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(()-> {for (int i = 1; i <= 5000; i++) {Deom15.func();}});Thread t2 = new Thread(()-> {for (int i = 1; i <=5000 ; i++) {Deom15.func();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = "+ count);
}

对于这种写法有个致命的缺点,一旦有多个线程调用func,则这些线程都会触发锁竞争。
4. 原因4引起的线程不安全——编译器优化产生的线程不安全

public static int count;
public static void main(String[] args) {Thread t1 = new Thread(() -> {while(count == 0) {//未执行操作}System.out.println("退出t1线程");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("输入一个数:");count = scanner.nextInt();});t1.start();t2.start();
}

在t1线程中while(count == 0)这个操作在指令的角度来分析就两个指令分别为load和cmp指令
load指令将内存中数据读入cpu中寄存器
cmp在cpu寄存器中进行比较
1.内存读取数据的速度远远小于寄存器读取的数据的速度就会造成load指令执行的速度远远慢于cmp指令执行。
2.在t2线程未修改count之前load指令执行的结果是一样的。
由上述两个原因,java编译器为了提高效率就会将load指令这个操作优化,所以当t2线程修改了count的值t1线程也不会感知到。
如果在t1线程的循环体中加一些I/O操作或者阻塞操作,这样java编译器就不会去优化load指令。

public static int count;
public static void main(String[] args) {Thread t1 = new Thread(() -> {while(count == 0) {try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("退出t1线程");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("输入一个数:");count = scanner.nextInt();});t1.start();t2.start();
}

如何解决由编译器优化引起的线程不安全?
1.上述在循环体中加I/O操作或者阻塞操作可以解决
2.用volatile关键字来修饰需要修改的变量——这个关键字只能解决编译器优化带来的内存可见性问题,不能解决原因三带来的问题。
volatile关键字
当为count加上volatile关键字就会告知编译器这个变量不能随便被优化`在这里插入代码片

public volatile static int count;
public static void main(String[] args) {Thread t1 = new Thread(() -> {while(count == 0) {}System.out.println("退出t1线程");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("输入一个数:");count = scanner.nextInt();});t1.start();t2.start();
}
  1. 原因5引起的线程不安全——编译器优化策略导致
    指令重排序旨在编译器在优化的时候将你的代码重新调整执行顺序来提高效率,优化前的逻辑与优化后的逻辑是等价的,在单线程中指令重排序这个优化策略是不会造成线程不安全,但是在多线程中就会导致线程不安全。
    面对指令重排序我们采取用volatile关键字
class SinlgetonLazy1 {private static volatile SinlgetonLazy1 instance = null;public static SinlgetonLazy1 getInstance() {Object locker = new Object();if(instance == null) {synchronized(locker) {if (instance == null ) {instance = new SinlgetonLazy1();}}}return instance;}private SinlgetonLazy1() {}
}
public class Deom24 {public static void main(String[] args) throws InterruptedException {SinlgetonLazy1 s = SinlgetonLazy1.getInstance();Thread t1 = new Thread(() -> {SinlgetonLazy1 s1 = SinlgetonLazy1.getInstance();});Thread t2 = new Thread(() -> {SinlgetonLazy1 s2 = SinlgetonLazy1.getInstance();});t1.start();t2.start();}
}

1.3三种典型死锁场景

场景一:锁是不可重入锁,并且一个线程针对一个锁对象,连续被加锁两次。
采取可重入锁(synchronized)就可以对这个问题迎刃而解了
场景二:两个线程两把锁
现有t1线程t2线程和locker1锁locker2锁, locker2锁要对t1线程中内容加锁,但同时locker2锁对t2线程还未解锁,所以t1线程需要阻塞等待,而现在locker1锁也要对t2线程中的内容加锁,但是locker1对t1线程还未解锁,所以t2线程需要阻塞等待,这样就导致你等我,我等你的死锁现象`

public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {synchronized (locker1) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2) {System.out.println("t1线程获取到了两把锁");}}});Thread t2 = new Thread(() -> {synchronized (locker2) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1) {System.out.println("t2线程获取到了两把锁");}}});t1.start();t2.start();
}

上述代码就是场景二死锁的实现代码
通过jconsole窗口观察:
在这里插入图片描述
此时就是t1线程想要获取到locker2锁,但locker2锁被t2线程给加锁了并且没有解锁,所以此时的t1线程想要获取locker2锁只能阻塞等待。
在这里插入图片描述
此时就是t2线程想要获取到locker1锁,但locker1锁被t1线程给加锁了并且没有解锁,所以此时的t2线程想要获取locker1锁只能阻塞等待。
场景三:N个线程M把锁——哲学家就餐问题
约定每一个哲学家必须先获取编号小的筷子,后获取编号大的筷子,就可以解决哲学家就餐问题。

1.4如何避免死锁问题

出现死锁的四大必要条件,少一个都不会出现死锁。

  1. 锁具有互斥特性(基本特点,一个线程拿到锁之后,其他线程就得阻塞等待)——基本特点
  2. 锁不可抢占(不可被剥夺)——基本特点
  3. 请求和保持(一个线程拿到一把锁之后,不释放这个锁的前提下,再尝试获取其他锁)——代码结构
  4. 循环等待(多个线程获取多个锁的过程中,出现了循环等待,A线程等待B线程,B线程又等待A线程)——代码结构
    要避免死锁,由于第一和第二点是锁的基本特性所以我们无法避免,我们只能从第三点和第四点出发避免死锁。
    针对第三点:我们尽量不要出现嵌套锁。
    针对第四点:我们可以约定加锁的顺序,让所有的线程按照加锁的顺序来获取锁。
    针对上述的代码出现了死锁,我们就可以约定加锁的先后顺序来避免死锁,我们约定locker1先加锁,locker2后加锁。
public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {synchronized (locker1) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2) {System.out.println("t1线程获取到了两把锁");}}});Thread t2 = new Thread(() -> {synchronized (locker1) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2) {System.out.println("t2线程获取到了两把锁");}}});t1.start();t2.start();
}

2.线程等待通知机制

2.1等待通知的作用

通过条件,判断当前逻辑是否能够执行,如果不满足条件不能执行,那么就主动进行阻塞(wait)让其他线程来调度cpu的资源,等到条件满足的时候,再让其它线程(阻塞的线程)来唤醒。

2.2等待通知的方法——wait

  1. wait方法是Object类提供,所以任何对象都能调用这个方法
  2. wait方法和sleep一样会被interrupt打断并且自动清空标志位
  3. wait方法不仅仅可一个阻塞等待还可以解锁
  4. wait方法要放在synchronized内部使用
public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() -> {synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}});

在这里插入图片描述

2.3唤醒wait的方法——notify

  1. notify方法要放在synchronized内部使用
  2. notify方法是Object类提供,所以任何对象都能调用这个方法wait方法是Object类提供,所以任何对象都能调用这个方法
  3. notify是随机唤醒被阻塞的线程
  4. notifyAll()方法可以唤醒所有被阻塞的线程,但这种方法不常用。`
public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() -> {System.out.println("t1线程等待之前");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t1线程等待结束");});Thread t2 = new Thread(() -> {System.out.println("t2线程唤醒t1线程之前");synchronized (locker) {try {Thread.sleep(3000);locker.notify();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t2线程唤醒结束");});t1.start();t2.start();
}

此时的t1线程的状态转化为:WAITTING——RUNNABLE——BLOCKED

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

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

相关文章

前端练习小项目——视觉冲击卡片

前言&#xff1a; 前言&#xff1a;在学习完HTML和CSS之后&#xff0c;我们就可以开始做一些小项目了&#xff0c;本篇文章所讲的小项目为——视觉冲击卡片 ✨✨✨这里是秋刀鱼不做梦的BLOG ✨✨✨想要了解更多内容可以访问我的主页秋刀鱼不做梦-CSDN博客 先让我们看一下效果&a…

maxKb+ollama+lama2-chinese 基于知识库+本地模型的知识问答系统

maxKbollamalama2-chinese 基于知识库本地模型的知识问答系统 搭建步骤 搭建maxKb docker run -d --namemaxkb -p 8080:8080 -v ~/.maxkb:/var/lib/postgresql/data cr2.fit2cloud.com/1panel/maxkb# 用户名: admin # 密码: MaxKB123..github的访问地址&#xff1a;https://…

Vant2组件库的基础应用

目录 一、Picker 选择器 1.1、数组对象处理 1.2、每个选项颜色设置 二、滚动分页加载列表 三、Calendar 日历(可选范围限制) 四、input值过滤 官网&#xff1a;Vant 2 - Mobile UI Components built on Vue 一、Picker 选择器 官网示例数据&#xff1a; columns: [杭州…

计算机网络实验之单交换机互联终端实验

1.网线 4对&#xff0c;8根&#xff0c;RJ-45连接器&#xff08;水晶头&#xff09;&#xff1b; &#xff08;1&#xff09;直通线 双绞线缆两端按照EIA/TIA568B规格连接水晶头&#xff0c;该双绞线为直通线。 橘白1&#xff0c;橘2&#xff0c;绿白3&#xff0c;蓝4&#…

WPF学习(2)--类与类的继承2-在窗口的实现

一、代码分析 1.Animal.cs 1.1 代码 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace AnimalNamespace {public class Animal{public string Name { get; set; }public int Age { get; set…

RabbitMQ的简单使用 —— Python篇

&#xff08;一&#xff09;RabbitMQ的简介 RabbitMq 是实现了高级消息队列协议&#xff08;AMQP&#xff09;的开源消息代理中间件。消息队列是一种应用程序对应用程序的通行方式&#xff0c;应用程序通过写消息&#xff0c;将消息传递于队列&#xff0c;由另一应用程序读取 完…

JavaWeb项目配置教程

将你的项目&#xff08;只有代码的文件&#xff0c;不是整个文件&#xff09;拖入idea 找到数据库配置代码&#xff08;一般在Util包里面&#xff0c;或者是properties配置文件&#xff09;并将密码修改为你的数据库密码。 点击Edit Configurations 点击Configure&#xff0…

SOLIDWORKS安装运行环境建议 慧德敏学

SOLIDWORKS是一款要求很高的软件。无可否认。您的电脑功能越强大&#xff0c;运行得越好&#xff0c;但是我们也要考虑购买成本&#xff0c;因此&#xff0c;选择正确的配置很重要。在选择用于SOLIDWORKS的电脑配置时&#xff0c;需要综合考虑多个方面以确保软件能够流畅、以更…

Hi3861 OpenHarmony嵌入式应用入门--PWM 三色灯

这篇文章是讲解的pwm控制三色灯的部分&#xff0c;这部分也是后续全彩智能灯的基础。 硬件原理如下 IO管脚定义在hi-12f_v1.1.2-规格书-20211202.pdf文档中 GPIO API API名称 说明 unsigned int IoTGpioInit(unsigned int id); GPIO模块初始化 hi_u32 hi_io_set_func(hi_i…

无引擎游戏开发(2):最简游戏框架 | EasyX制作井字棋小游戏I

一、EasyX中的坐标系 不同于数理中的坐标系&#xff0c;EasyX中的y轴是竖直向下的 二、渲染缓冲区 之前的程序添加了这三个函数改善了绘图时闪烁的情况: 小球在"画布“上移动的过程就是我们在调用绘图函数&#xff0c;这个”画布“就是渲染缓冲区&#xff0c;先绘制的内…

【配置】Notion自动化备份到github方案

步骤 打开notion网页&#xff0c;获取到需要的值 token_v2 找到请求getSpaces的 Cookie 值 token_v2 space_id 找到请求getSpaces的响应结果space,如下图&#xff1a; file_token 找个页面点击导出&#xff0c;之后拿到这个配置项 注意&#xff1a;配置项会过期&#xff0c…

基于一种改进熵方法的旋转机械故障诊断模型(MATLAB)

熵的概念起源于热力学&#xff0c;1884年&#xff0c;玻尔兹曼定义熵&#xff0c;用以描述分子热运动的无序性和混乱度。1948年&#xff0c;Shannon在其发表的《AMathematicalTheoryofCommunication》中提出香农熵&#xff0c;首次将“熵”引入信息度量范畴&#xff0c;为信息论…

RK3568技术笔记十三 Ubuntu的编译

Ubuntu文件系统编译 在编译前需要按照前面的方法初始化编译环境&#xff0c;否则会导致编译失败&#xff08;若配置过则无需重复配置&#xff09;。 按下述方法编译的Ubuntu系统&#xff0c;用户名是&#xff1a;dianyu 密码&#xff1a;1 编译Ubuntu&#xff0c;执…

机械臂 CoppeliaSim Simulink联合仿真

实现机械臂在CoppeliaSim&#xff08;以前称为V-REP&#xff09;和Simulink上的联合仿真涉及多个步骤&#xff0c;包括环境设置、模型导入、通信配置、控制算法设计和测试调试。 前期准备 安装软件配置工作环境创建和配置CoppeliaSim场景 导入机械臂模型配置机械臂参数在Simuli…

webp图片格式怎么转换成jpg?值得收藏的四种转换方法介绍!

webp图片格式怎么转换成jpg&#xff1f;在当今数字图像处理领域&#xff0c;新兴的WebP格式以其独特的特性和兼容性&#xff0c;迅速成为备受追捧的图像格式之一&#xff0c;这一格式以其高效的压缩能力和优秀的网络传输速度著称&#xff0c;为网络图像的传输和存储提供了全新的…

一种稀疏贝叶斯学习的旋转机械故障诊断方法(MATLAB)

轴承的故障诊断技术是通过检测轴承故障特征信息来判断轴承的具体故障为位置或损伤程度。在轴承发生损坏时&#xff0c;故障特征信息会随着工作时间的增长变得明显。轴承的损坏过程可以分为四个阶段。第一个阶段为损伤初始阶段&#xff0c;轴承故障特征信号一般无法测量。第二个…

基于SSM+Jsp的书店仓库管理系统

摘要&#xff1a;仓库作为储存货物的核心功能之一&#xff0c;在整个仓储中具有非常重要的作用&#xff0c;是社会物质生产的必要条件。良好的仓库布局环境能够对货物进入下一个环节前的质量起保证作用&#xff0c;能够为货物进入市场作好准备&#xff0c;在设计中我们根据书店…

【人工智能】音乐大模型的深入探讨——当机器有了创意,是机遇还是灾难?

&#x1f440;国内外音乐大模型基本情况&#x1f440; ♥概述♥ ✈✈✈如FreeCompose、一术科技等&#xff0c;这些企业专注于开发人工智能驱动的语音、音效和音乐生成工具&#xff0c;致力于利用核心技术驱动文化产业升级。虽然具体公司未明确提及&#xff0c;但可以预见的是…

产业园区空间优化设计的创新实践者

树莓集团在产业园区运营中的空间优化设计方面&#xff0c;通过全面规划与科学布局、绿色智能与可持续发展、个性化定制与灵活多变、创新实践与数字化升级等措施&#xff0c;为企业提供了高品质、高效率的空间环境和服务支持。 一、全面规划与科学布局 明确产业定位&#xff1a…

Nuxt3 实战 (十一):添加路由 Transition 过渡效果和 Loading 动画

页面过渡效果 Nuxt3 利用 Vue 的 组件 在页面和布局之间应用过渡效果。 nuxt.config.ts 文件配置&#xff1a; export default defineNuxtConfig({app: {pageTransition: { name: page, mode: out-in }}, })在页面之间添加过渡效果&#xff0c;在 app.vue 文件中添加以下 CS…