【JavaEE 初阶(二)】线程安全问题

❣博主主页: 33的博客❣
▶️文章专栏分类:JavaEE◀️
🚚我的代码仓库: 33的代码仓库🚚
🫵🫵🫵关注我带你了解更多线程知识

在这里插入图片描述

目录

  • 1.前言
  • 2.synchronized
    • 2.1例子
    • 2.2synchronized修饰代码块
    • 2.3 synchronized修饰方法
    • 2.4synchronized特性
  • 3.死锁
    • 3.1死锁的成因
    • 3.2解决死锁
  • 4.volatile
    • 4.1内存可见性问题
    • 4.2volatile解决
  • 5.wait与notify
    • 5.1wait
    • 5.2notify
  • 6.总结

1.前言

在上一篇文章中,我们已经初步认识了线程的一些知识,但线程中一个重要问题就是线程安全问题,这篇文章我们就来了解为什么会引起线程安全问题,已经解决方法,有些代码在单个线程中执行是完全正常的,不会出现bug,但同样的代码,让多个线程,同一时间执行那么就可能出现bug,这就称为线程安全问题。


2.synchronized

2.1例子

例:我们让两个线程同时执行cou++操作,各自增5w,预期结构应该为10w,我们通过代码来观察是否符合预期结果。

public class Demo11 {public static int count=0;public static void main(String[] args) throws InterruptedException {Object lock=new Object();//Object lock2=new Object();Thread t1=new Thread(()->{for (int i=0;i<50000;i++){                count++;             }});Thread t2=new Thread(()->{for (int i=0;i<50000;i++){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count="+count);}
}

观察结果:
在这里插入图片描述
我们多次运行发现发现每次count的结尾都不等于10w,并且每次都不同。
出现这样的原因是为什么呢?其实是因为在执行cou++操作的时候有3步操作

load 把数据从内存读到cpu中
add 把寄存器+1
save 把寄存器中的数据保存到内存中
那么两个进程同时执行就会出现多种方式:
在这里插入图片描述
博主列出的只是部分执行情况,实际情况还有更多
在正确情况下:
在这里插入图片描述
错误情况下:
在这里插入图片描述
我们就可以知道线程安全的原因:
1.操作系统种线程的调度是随机的
2.两个线程对于同一个变量进行修改
3.修改操作不是原子性的
4.内存可见性问题
5.指令重排序问题
如果要想解决线程安全问题,就可以使修改操作变为原子性的,那么怎么变为原子性的呢?加锁操作。
最常见的加锁方法就是synchronized关键字。

2.2synchronized修饰代码块

在使用synchronized时,要搭配一个代码块{}进入{就会加锁,出了}就会解锁,我们用代码进行实现。
在这里插入图片描述
我们发现synchronized()报错,是因为()中需要表示一个用来加锁的对象,这两个对象是啥并不重要,重要的是通过这个对象来区分两个线程是否在竞争同一个锁,如果两个线程在针对同一个对象加锁就会出现锁竞争,那么由于锁的竞争,只有等一个线程解锁后,另一个线程才能再进行count++操作。

public class Demo11 {public static int count=0;public static void main(String[] args) throws InterruptedException {Object lock=new Object();//Object lock2=new Object();Thread t1=new Thread(()->{for (int i=0;i<50000;i++){synchronized (lock){count++;}}});Thread t2=new Thread(()->{for (int i=0;i<50000;i++){synchronized (lock){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count="+count);}
}

2.3 synchronized修饰方法

synchronized除了修饰代码块以外还可以修饰方法

class cunter{int count;public synchronized void count (){//相当于synchronized(this){}count++;}
//修饰静态方法
//    public static synchronized void count2 (){//相当于synchronized(cunter.class){}
//        count++;
//    }
}
public class Demo12 {public static void main(String[] args) throws InterruptedException {cunter counter=new cunter();Thread t1=new Thread(()->{for (int i=0;i<50000;i++){counter.count();}});Thread t2=new Thread(()->{for (int i=0;i<50000;i++){counter.count();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count="+counter.count);}
}

synchronized用的锁存在java对象头里面的,在一个java对象中,除了自己定义的属性和方法,还有一些自带的属性,这些自带的属性就称为对象头,其中就有属性表示当前对象是否加锁。

2.4synchronized特性

synchronized特性

1.互斥:某一个线程a如果执行某个某个对象的加锁操作时,如果其他线程也想给同一个对象加锁,那么就要 等a执行完成b才能实现加锁操作。
2.可重入:1个线程中,synchronized 代码块中针对同一把锁加锁多次,不会出现“死锁”问题。

public class Demo15 {public static void main(String[] args) {Object lock=new Object();Thread t1=new Thread(()->{synchronized (lock){synchronized (lock){System.out.println("t1");}}//(1)});//(2)t1.start();}
}

上诉代码,在t线程如果第一次的lock加锁成功,又遇到了一个lock操作,但只有等第一次}(2)de的位置解锁才能执行加锁操作,可是如果使}(2)执行完,就需要先执行加锁操作,这样就导致代码一直注释,没有办法释放锁。所以就把synchronized设置为了“可重入锁”就解决了上述问题。
但此时又有了新的问题,如果在一个线程中,对一把锁多次加锁,那么在什么时候才释放锁呢?

public class Demo15 {public static void main(String[] args) {Object lock=new Object();Thread t1=new Thread(()->{synchronized (lock){synchronized (lock){synchronized (lock){synchronized (lock){synchronized (lock){}}}}}});t1.start();}
}

要在这个线程的最外层才能释放锁,在锁对象中,不仅会记录是谁拿到了锁,还会记录加锁了多少次,每加锁一次,计数器++,解锁一次,计数器–,直到最后一个大括号结束。

3.死锁

在上述代码中,我们已经提到过死锁了,在1个线程中,针对一把锁连续加锁两次,如果是不可重入,就会出现死锁了。
如果是两个线程,两把锁(无论是不是可重入,都会死锁)
例如:(1)t1获取锁A,t2获取锁B (2)t1获取锁B,t2获取锁A

public class Demo13 {public static void main(String[] args) {Object lock1=new Object();Object lock2=new Object();Thread t1=new Thread(()->{synchronized (lock1){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock2){System.out.println("t1线程");}}});Thread t2=new Thread(()->{synchronized (lock2){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock1){System.out.println("t2线程");}}});t1.start();t2.start();}
}

在t1线程,lock1锁中在等待lock2锁解锁,但此时lock2也在等待lock1解锁,此时会会两个线程一直僵持下去。
N个线程M把锁:哲学家问题
有5个哲学家坐在一起吃饭,但只有5根筷子,哲学家就只做两件事情,一件事情为思考,另一件事情就是吃饭,当其中一个哲学家要吃饭时,就会拿起左右两边的筷子,那么此时如果左右相邻的哲学家也想吃饭时,就需要等待正在吃饭的哲学家吃完饭,放下筷子,才能继续吃,在通常情况下,整个系统可以很好的运转,但是当5个哲学家同时拿起左边的筷子时,就会出现死锁问题.
在这里插入图片描述
死锁是一种严重的bug那么该如何解决死锁问题呢?我们就需要先了解死锁的成因。

3.1死锁的成因

1.互斥使用(锁的基本特性):当一个线程有一把锁时,另一个线程也想获取同一把锁就要阻塞等待。
2.不可抢占(锁的基本特性):当线程a拿到锁时,只有等线程a解除锁,线程b才能再使用。
3.请求保持:一个线程尝试获取多把锁
4.循环等待:等待的依赖关系形成了环。

3.2解决死锁

互斥和不可抢占性都是锁的基本特性,我们可以通过代码的结果来来避免写成“嵌套锁”但这个方案不一定好使,有的需求可能就是需要进行这种嵌套操作,所以我们最好
针对循环来解决,可以约定加锁条件避免形成循环等待,针对锁,约定加多把锁的时候,现加编号小的锁,再加编号大的锁并且所有线程都要遵守这一规则。

public class Demo13 {public static void main(String[] args) {Object lock1=new Object();Object lock2=new Object();Thread t1=new Thread(()->{synchronized (lock1){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock2){System.out.println("t1线程");}}});Thread t2=new Thread(()->{synchronized (lock1){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock2){System.out.println("t2线程");}}});t1.start();t2.start();}
}

4.volatile

4.1内存可见性问题

计算机运行程序,经常要访问数据,这些数据往往存储在内存中,cpu使用这些变量的时候,要先从内存中读取数据,再对数据进行操作,cpu读取内存相对来说是非常慢的,cpu执行大部分操作都是非常快的,但一旦涉及到读取内存操作,就非常慢。为了解决上诉问题,此时编译器就可能对代码进行优化,把一些本来要读取内存的操作优化为读取寄存器,减少内存的读取次数就大大提高了程序的效率。
例:

public class Demo14 {public static int isQuit=0;public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{while (isQuit==0){}System.out.println("t1进程结束");});t1.start();Thread.sleep(1000);System.out.println("请输入isQuit");Scanner scanner=new Scanner(System.in);isQuit= scanner.nextInt();}
}

预期效果是当过了1s中后,输入isQuit为1,应该结束循环,输出t1进程结束。
我们来看一看实际结果:
在这里插入图片描述
很明显,实际结果和预期结果不一样,之前是两个进程修改同一个变量引起的bug,但现在是一个线程修改,另一个线程读,同样也引起了bug,是什么原因呢?
在t1线程中读取isQuit的值到寄存器中,通过cmp指令比较寄存器的值是否为0,由于这个循环执行的飞快,就需要多次从内存中load,再cmp,此时编译器就发现虽然进行了这么多次load但是load出来的结果没有任何变化,所以编译器就做了一个大胆的决定!只是第一次寻黄的时候读取内存,此后直接从寄存器中读取isQuit的值。它的初心虽然是好的,但是我此后如果修改了isQuit的值,但t1寄存器读取的仍然是isQuit修改前的值就出现了bug。这个问题就称为“内存可见性”问题。

4.2volatile解决

在多线程环境下,编译器对是否要进行优化的判定不一定准就需要通过volatile关键字告诉编译器我不需要优化!!!!

import java.util.Scanner;
public class Demo14 {public static volatile  int isQuit=0;public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{while (isQuit==0){}System.out.println("t1进程结束");});t1.start();Thread.sleep(1000);System.out.println("请输入isQuit");Scanner scanner=new Scanner(System.in);isQuit= scanner.nextInt();}
}

此时就可以结束线程1了:
在这里插入图片描述

5.wait与notify

5.1wait

wait是Object的一个方法,wait是使进程变为阻塞状态。
我们通过代码来进行演示:

public class Demo20 {public static void main(String[] args) throws InterruptedException {Object object=new Object();System.out.println("wait之前");object.wait();System.out.println("wait之后");}
}

我们发现运行时依然有错:非法监视器状态异常,监视器就是指的sychronized。
在这里插入图片描述
wait在执行的时候只做三件事情:
1.释放锁资源
2.让线程进入阻塞状态
3.当线程被唤醒重新获取锁
对代码进行修改:

public class Demo20 {public static void main(String[] args) throws InterruptedException {Object object=new Object();System.out.println("wait之前");synchronized (object){object.wait();}System.out.println("wait之后");}
}

在这里插入图片描述

5.2notify

这时我们会发现wait会一直持续等待,知道有其他线程调用notify唤醒它。
notify是一次唤醒一个进程,而notifyAll是一次唤醒所有进程。

public class Demo16 {public static void main(String[] args) throws InterruptedException {Object object=new Object();Thread t=new Thread(()->{try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (object){System.out.println("进行通知");object.notify();}});t.start();System.out.println("wait之前");synchronized (object){object.wait();}System.out.println("wait之后");}
}

在这里插入图片描述
wait除了默认的无参版本,还有一个带参的版本,但参版本就是指定超时时间避免无休止等待。

6.总结

本篇文章主要介绍了sychronized加锁操作,死锁的成因,死锁的解决,内存可见性问题以及内存可见的解决方案,最后介绍了wait和notify的运用。

下期预告:多线程代码案例

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

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

相关文章

分布式领域计算模型及SparkRay实现对比

目录 一、分布式计算领域概览 二、Spark计算模型分析 三、Ray计算模型分析 3.1 需求分析 3.2 系统设计 3.3 系统实现 四、总结 一、分布式计算领域概览 当前分布式计算模型主要分为以下4种&#xff1a; Bulk Synchronous Parallel Model&#xff08;块同步并行模型&…

【Linux 11】进程地址空间

文章目录 &#x1f308; Ⅰ 虚拟地址引入&#x1f308; Ⅱ 虚拟地址空间&#x1f308; Ⅲ 页表 (解释 fork() 的返回值既 > 0 又 0)&#x1f308; Ⅳ 什么是地址空间&#x1f308; Ⅴ 为什么要有地址空间 &#x1f308; Ⅰ 虚拟地址引入 现在通过一段代码来观察一个现象&a…

初识指针(1)<C语言>

前言 指针是C语言中比较难的一部分&#xff0c;大部分同学对于此部分容易产生“畏难情结”&#xff0c;但是学习好这部分对C语言的深入很大的帮助&#xff0c;所以此篇主要以讲解指针基础为主。 指针概念 变量创建的本质就是在内存中申请空间&#xff0c;找到这个变量就需要地址…

交互中的“互”难以产生的原因

脑机交互技术的目标是通过分析和解读大脑活动&#xff0c;将其与特定的意图、指令或行为连接起来。通过训练和分析&#xff0c;可以建立起大脑活动与特定行为或意图之间的关联模型&#xff0c;从而实现脑机交互的应用&#xff0c;例如控制外部设备、传递信息等。然而&#xff0…

Python机器学习实验 Python 数据可视化

1.实验目的 掌握 Matplotlib 数据可视化的常用方法。 2.实验内容 1. 绘制鸢尾花数据集的特征分布图 说明&#xff1a;鸢尾花是单子叶百合目花卉&#xff0c;是一种比较常见的花&#xff0c;鸢尾花的品种较多。 鸢尾花数据集最初由 Edgar Anderson 测量得到&#xff0c;而后在…

深入学习和理解Django模板层:构建动态页面

title: 深入学习和理解Django模板层&#xff1a;构建动态页面 date: 2024/5/5 20:53:51 updated: 2024/5/5 20:53:51 categories: 后端开发 tags: Django模板表单处理静态文件国际化性能优化安全防护部署实践 第一章&#xff1a;模板语法基础 Django模板语法介绍 Django模…

每天五分钟深度学习:数学中常见函数中的导数

本文重点 导数是微积分学中的一个核心概念,它描述了函数在某一点附近的变化率。在物理学、工程学、经济学等众多领域中,导数都发挥着极其重要的作用。本文旨在详细介绍数学中常见函数的导数,以期为读者提供一个全面而深入的理解。 数学中常见的导数 常数函数的导数 对于常数…

ctfshow 框架复现

文章目录 web 466web 467web 468web469web 470web 471web 472web 473web 474web 475web 476 web 466 Laravel5.4版本 &#xff0c;提交数据需要base64编码 代码审计学习—Laravel5.4 - 先知社区 (aliyun.com) 用第二条链子 反序列化格式 /admin/序列化串base64<?php na…

(论文阅读-多目标优化器)Multi-Objective Parametric Query Optimization

目录 摘要 一、简介 1.1 State-of-the-Art 1.2 贡献和大纲 二、定义 三、相关工作 四、问题分析 4.1 分析 4.2 算法设计影响 五、通用算法 5.1 算法概述 5.2 完备性证明 六、分段线性代价函数算法 6.1 数据结构 6.2 基本运算实现 6.3 复杂度分析 七、实验评估 …

FR-TSN4206获得“时间敏感网络产业链名录计划”测试认证证书,TSN交换机助力智能工业发展

TSN技术&#xff0c;即时间敏感网络技术&#xff0c;已成为智能工业、自动驾驶等领域的核心。它通过时钟同步、数据调度等功能&#xff0c;确保低延迟、高可靠性的数据传输。 为推动TSN技术在我国的发展&#xff0c;工业互联网产业联盟联合多家单位启动了“时间敏感网络产业链名…

Amazon EKS创建EFS存储卷

1、创建Amazon EFS CSI 驱动程序 亚马逊相关文档 在 Select trusted entity&#xff08;选择受信任的实体&#xff09;页面上操作 在 Add permissions&#xff08;添加权限&#xff09;页面上筛选AmazonEFSCSIDriverPolicy操作 记得将AmazonEBSVolumePolicy添加到我们创建的…

Colab/PyTorch - Getting Started with PyTorch

Colab/PyTorch - Getting Started with PyTorch 1. 源由2. 概要2.1 PyTorch是什么&#xff1f;2.2 为什么学习PyTorch&#xff1f;2.3 PyTorch库概览 3. 步骤4. 预期&展望5. 总结6. 参考资料 1. 源由 世界在发展&#xff0c;为其服务的技术也在不断演变。每个人都要跟上技…

Docker-Compose 容器集群的快速编排

Docker-compose 简介 Docker-Compose项目是Docker官方的开源项目&#xff0c;负责实现对Docker容器集群的快速编排。 Docker-Compose将所管理的容器分为三层&#xff0c;分别是 工程&#xff08;project&#xff09;&#xff0c;服务&#xff08;service&#xff09;以及容器&…

2024阿里云ctf-web-chain17学习

agent jdk17依赖有h2思路清晰打jdbc attack <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/com.aliba…

AI图书推荐:ChatGPT在真实商业世界中的应用

《ChatGPT在真实商业世界中的应用》 (Unleashing The Power of ChatGPT: A Real World Business Applications)首先概述了ChatGPT及其在对话式人工智能领域的影响。接着&#xff0c;你将深入了解ChatGPT的技术方面&#xff0c;理解机器学习算法和自然语言处理如何在后台工作。然…

Raft共识算法笔记,MIT6.824,

处理leader和follow的一个重要思路是多数投票&#xff0c;确保系统中存在奇数个服务器&#xff08;例如3台&#xff09;。进行任何操作都需要来自多数服务器的同意&#xff0c;例如3台服务器中的2台。如果没有多数同意&#xff0c;系统会等待。为什么多数投票有助于避免脑裂问题…

【Linux】目录和文件相关的命令,补充:centos7系统目录结构

【Linux】Linux操作系统的设计理念之一就是“一切皆文件”&#xff08;Everything is a file&#xff09;&#xff0c;即将设备、文件等都当作“文件”处理。 “文件”主要类型有&#xff1a;目录&#xff08;即文件夹&#xff09;&#xff0c;链接文档&#xff08;即快捷方式…

【论文复现】Graph Attention Networks图注意力神经网络

图注意力神经网络 前言一、论文解读1.1 模型架构1.2 数学推导 二、代码复现2.1 数据准备2.1.1 数据转化2.1.2 创建数据集 2.2 模型构建2.2.1 参数设置2.2.2 模型代码2.2.3 pytorch官方GAT源码实现 2.3 模型训练 三、结果展示3.1 复现结果3.2 论文结果 四、代码细节代码链接 前言…

【Python项目】基于opencv的的【疲劳检测系统】

技术简介&#xff1a;使用Python技术、OpenCV图像处理库、MYSQL数据库等实现。 系统简介&#xff1a;用户可以通过登录系统平台实现实时的人脸照片的拍摄和上传&#xff0c;结合上传图像的内容进行后台的图像预处理和运算分析&#xff0c;用户可以通过照片分析界面查看到当前检…

数学中的极值

在数学领域中&#xff0c;极值是一个重要的概念&#xff0c;它不仅在纯数学理论研究中占据核心地位&#xff0c;还在实际应用中发挥着巨大作用。从微积分的基本定理到优化问题的求解&#xff0c;从物理学的能量守恒到经济学的边际分析&#xff0c;极值理论无处不在。本文将详细…