[Java EE] 多线程(三):线程安全问题(上)

news2025/1/16 17:58:57

1. 线程安全

1.1 线程安全的概念

如果多线程环境下代码运行的结果不符合我们的预期,则我们说存在线程安全问题,即程序存在bug,反之,不存在线程安全问题.

1.2 线程不安全的原因

我们下面举出一个线程不安全的例子:我们想要在两个线程中对count进行++操作

public class Demo9 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        thread.start();
        thread1.start();
        thread.join();
        thread1.join();
        System.out.println(count);
    }
}

运行结果如下:
在这里插入图片描述
但是这里我们预期的结果是100000,这里我们看到,实际结果和预期结果相差甚远,这便是产生了线程安全问题,使得程序出现了bug,我们要想解决上述的bug,我们必须先了解清楚bug产生的原因.

  1. 线程调度是随机的
    这是线程安全问题的罪魁祸首
    随机调度使⼀个程序在多线程环境下,执行顺序存在很多的变数.
    程序猿必须保证在任意执行顺序下,代码都能正常工作.
    某个线程在执行指令的过程中,当他执行任何一个指令的时候,都有可能被其他线程抢占走CPU.
  2. 修改共享数据
    多个线程同时修改同一个变量.上面的代码中,就都是针对count进行修改.
  3. 原子性
    在前面,我们有给大家提到过事务的原子性,大家还记得我们的助教迪卢克姥爷吗?
    在这里,多线程的原子性其实和事务的原子性大相径庭.我们在这里首先要理解什么是多线程中的原子性:

有请助教:达达利亚,钟离
达达利亚和钟离都到了一台ATM机前来取钱,现在每一台ATM机前都有一个门,一把锁,当达达利亚进去之后,门就会自动上锁,这样钟离便不会对达达利亚取钱的过程造成干扰,在达达利亚取完钱之前,钟离只可以在外面排队等待,在达达利亚取完钱之后,钟离才可以进入.也就是在tread线程对count进行修改的时候,tread1线程不可以对tread修改count的过程进行干扰,这便保证了原子性.反之如果钟离对达达利亚取钱的过程造成了干扰,这便不保证原子性.
在这里插入图片描述

一条Java语句不一定是原子的,也不一定是一条指令:
我们回到线程这里,那么如果拿上面这个存在线程安全问题的代码(不保证原子性的代码),那么他的底层原理是什么样子的呢:

  • 首先tread和tread1同时读到count=0
  • tread线程对count进行++之后放入内存之后,count变为1
  • tread1对线程进行++之后,对上一个count=1的值进行了覆盖,count还是1.
  • 这便会引起bug
    在这里插入图片描述
  1. 内存可见性
  2. 指令重排序
    后续介绍

1.3 解决线程安全问题

要想解决线程安全问题,我们必须要从原因来入手:

  • 从原因一入手:这是多线程已有的特性,无法干预.
  • 从原因二入手:这是一个切入点,**但是不普适,只针对特殊的场景可以做到,**比如String把变量设置为不可变对象,就是为了保证线程安全问题.在对上一个String进行修改的时候,其实在底层又new了一个新的String,修改的实际上不是同一个变量.
  • 从原因三入手:这是一个普适性比较高的切入点,我们想象,我们是否也可以有一把向ATM机那样的锁,来保证线程的原子性呢,答案是有.我们可以使用synchronized关键字来对线程进行上锁.通过上锁操作来把非原子的操作打包为一个原子的操作.保证tread线程对count计算的结果写入内存中在tread1线程读取内存中的count之后,使得它们呈现串行化执行.
  • 从原因四和五入手,后续介绍.

1.4 synchronized关键字—>监视器锁

为了解决上述线程安全问题,我们使用synchronized对上述代码的线程进行加锁:

public class Demo11 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();//锁对象
        Thread thread = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (o) {//线程上锁
                    count++;
                }
            }
        });
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (o){//拿到的都是o锁,产生锁互斥
                    count++;
                }
            }
        });
        thread.start();
        thread1.start();
        thread.join();
        thread1.join();
        System.out.println(count);
    }
}
  • 首先,什么是锁:
    锁本质上是一个OS提供的功能,通过API给到了应用程序,JVM再对这样的API进行包装.这里我们就可以把锁简单地理解为一个不管类型,不管名字,不管是否存在泛型的任意变量,作用上有且只有一个,就是用来区分两个线程知否针对同一个对象加锁.
  • 如何对线程上锁:
    在一个线程中,在某一行使用synchronized ( )关键字,并在括号中传入锁对象,就证明从这一行的{开始,就开始对线程进行了上锁,直到}解锁.

当我们了解完synchronized的第一个特性之后,我们就知道上述上锁的过程是怎么回事了.

1.4.1 synchronized的使用实例

  1. 修饰代码块
public class SynchronizedDemo {
   private Object locker = new Object();
 
   public void method() {
  	 synchronized (locker) {
 //一系列操作
 	  }
   }
}
  1. 锁当前对象
public class SynchronizedDemo {
	 public void method() {
	 synchronized (this) {
 
		}
	}
}
  1. 直接修饰普通方法
public class SynchronizedDemo {
	 public synchronized void methond() {
	 }
}

一旦有线程调用该方法,就会上锁.

  1. 修饰静态方法
public class SynchronizedDemo {
 	public synchronized static void method() {
	 }
}

1.4.2 synchronized的特性

  1. 互斥性与锁竞争
    在tread线程对count进行++的时候,在count++的外围,我们使用synchronized关键字对count++进行了包裹,由于tread线程启动比tread1早,也就是在此时,线程tread已经拿到了o这把锁.此时由于tread1线程也在RUNNABLE状态,它也想拿到o这把锁.但是发现,o这把锁已经被tread线程占用了,只能阻塞等待,等待tread解锁.tread1进入BLOCKED状态.此时锁就产生了互斥性.
    解锁之后,由于系统调度线程的随机性,tread和tread1继续竞争o锁,便会产生锁竞争.
    在这里插入图片描述

我们举个例子来说明:
有请助教: 小乔,周瑜,兰陵王
由于兰陵王比周瑜先到一步,所以小乔先和兰陵王贴贴 了一段时间.在这里插入图片描述
兰陵王完事之后,兰陵王对小乔解锁,但是兰陵王又觉得自己还没有和小乔贴贴够,但是周瑜又向进去和自己的爱人贴贴,此时兰陵王和周瑜便产生了锁竞争,谁都向对小乔上锁.
在这里插入图片描述

如果两个线程对于两个不同的锁进行引用加锁,也就不会出现锁竞争问题:

public class Demo12 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object o1 = new Object();
        Object o2 = new Object();
        Thread thread = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (o1) {//o1对线程上锁
                    count++;
                }
            }
        });
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (o2){//拿到的是o2锁,不会产生锁互斥
                    count++;//线程安全问题仍然存在
                }
            }
        });
        thread.start();
        thread1.start();
        thread.join();
        thread1.join();
        System.out.println(count);
    }
}

但是这样还是会产生线程安全问题.
运行结果:
在这里插入图片描述

讨论:join()和上锁的区别
join是在tread全部执行完成之后,再去执行tread1,而加锁是并发执行.
在join等待的时候是WAITING状态,而在上锁过程中是BLOCKED状态.在这里插入图片描述在这里插入图片描述

  1. 可重入与不可重入(死锁)
    我们思考这样几个场景:
  • 场景一:一个线程一把锁
public class Demo13 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        Thread thread = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (o) {//对线程上锁
                    synchronized (o){//又上了一次锁
                        count++;
                    }
                }
            }
        });
        thread.start();
        thread.join();
        System.out.println(count);
    }
}

上面的代码tread两次利用o上锁,我们来思考,在第二次上锁的时候,会不会因为锁的互斥性,而使得tread线程产生阻塞,那就自己把自己锁死了,产生便了死锁.

举例说明:
有请助教:钟离
假如钟离在上厕所…在这里插入图片描述

如果产生上述情况,我们称该锁为不可重入锁.如c++,python中自带的锁,都是不可重入锁,一旦像上面那样写,就锁死了.
但是Java中的锁是可重入锁,对一个线程使用相同的锁进行多次加锁之后,不会出现锁死的情况.不会产生锁冲突.可见Java的创始者为了不让我们Java程序员写出bug,真的是操碎了心!!!
在这里插入图片描述
可重入锁的原理:
在可重⼊锁的内部,包含了"线程持有者"和"计数器"两个信息.
• 如果某个线程加锁的时候,先判断这个线程是否被加锁,如果没有,则加锁,如果发现锁已经被⼈占⽤,但是恰好占⽤的正是⾃⼰,那么仍然可以继续获取到锁,并让计数器⾃增.
• 解锁的时候计数器递减为0的时候,才真正释放锁.(才能被别的线程获取到)

举例说明:
有请助教:小乔,周瑜,兰陵王

在这里插入图片描述
在这里插入图片描述

那么什么时候Java会产生死锁呢?

  • 场景二:两个线程两把锁
public class Demo14 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        Object o1 = new Object();
        Thread thread = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (o) {//1.拿到o锁
                    synchronized (o1){//3.与tread1的o1锁互斥
                        count++;
                    }
                }
            }
        });
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (o1){//2.拿到o1锁
                    synchronized (o){//4.与tread的o锁互斥
                        count++;
                    }
                }
            }//3,4相互等待,最终卡死
        });
        thread.start();
        thread1.start();
        thread.join();
        thread1.join();
        System.out.println(count);
    }
}

运行结果:毛都没有!
在这里插入图片描述
这就说明,这里的的进程卡死,产生了死锁.
什么原理呢?由于tread一首先启动,tread拿到o锁,并上锁,此时tread1启动,拿到o1锁,当tread想要拿到o1锁的时候,发现o1锁被占用,阻塞等待,当tread1想要拿到o锁的时候,发现o锁被占用,阻塞等待,这时候tread1和tread相互循环相互等待,就产生了死锁.
这就像两个相互暗恋的人一样,都彼此暗恋着对方,但是都不敢鼓起勇气去表白,这样就会彼此错过.
在这里插入图片描述

  • 那么我们如何规避死锁呢?(重点面试题)
    首先我们要知道参数死锁的4个必要条件:
  1. 锁具有互斥性
  2. 锁不可剥夺
    上述是锁的两个基本的特性,我们无法干预
  3. 请求锁和保持锁
    一个线程拿到一把锁之后,不释放这个锁,就尝试获取其他锁.
  4. 循环等待:
    多个线程获取多个锁过程中,A等待B,B等待A.
    上述两个条件,我们都可以通过干预代码结构来解除死锁.
    我们需要约定好加锁顺序,让所有的线程按照一定的顺序加锁.
    我们尝试使用上面的方法对上面的场景二的死锁进行解除:调换lock1和lock2的位置,让tread执行完所有的逻辑之后释放锁之后,再轮到tread1执行.
public class Demo14 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        Object o1 = new Object();
        Thread thread = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (o) {
                    synchronized (o1){
                        count++;
                    }
                }
            }
        });
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (o){
                    synchronized (o1){
                        count++;
                    }
                }
            }
        });
        thread.start();
        thread1.start();
        thread.join();
        thread1.join();
        System.out.println(count);
    }
}

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

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

相关文章

RC电路延时时间常数在线计算器

RC电路延时时间常数在线计算器: https://www.838dz.com/calculator/1888.html 急用时&#xff0c;找不到。

后端通过@jsonformat格式化数据转发,前端无法正确显示

后端发送给前端的updatatime是有格式的 后端接收的数据没有任何变化&#xff0c;前端代码也很正常 显示时间也乱码 原因应该是某个注释和jsonformat冲突了&#xff0c;所幸就不用jesonformat 用手动配置的消息转换器 // 消息转换器&#xff0c;后端返回给前端数据格式化Overri…

用斐波那契数列感受算法的神奇(21亿耗时0.02毫秒)

目录 一、回顾斐波那契数列 二、简单递归方法 &#xff08;一&#xff09;解决思路 &#xff08;二&#xff09;代码展示 &#xff08;三&#xff09;性能分析 三、采用递归HashMap缓存 &#xff08;一&#xff09;解决思路 &#xff08;二&#xff09;代码展示 &…

C++入门----内联函数auto范围fornullptr指针

1.内联函数 顾名思义&#xff0c;内联函数也是函数的一种&#xff0c;我们在C语言的学习过程里面知道了函数和宏之间的区别和各自的优缺点&#xff1b; 函数的使用需要建立栈帧&#xff0c;宏的使用需要考虑各种符号的优先级问题&#xff0c;很容易出错&#xff0c;因为宏在使…

新恒盛110kV变电站智能辅助系统综合监控平台+道巡检机器人

江苏晋控装备新恒盛化工有限公司是晋能控股装备制造集团有限公司绝对控股的化工企业&#xff0c;公司位于江苏省新沂市。新恒盛公司40•60搬迁项目在江苏省新沂市经济开发区化工产业集聚区苏化片区建设&#xff0c;总投资为56.64亿元&#xff0c;该项目是晋能控股装备制造集团重…

Spring - 5 ( 8000 字 Spring 入门级教程 )

一&#xff1a;Spring IoC&DI 1.1 方法注解 Bean 类注解是添加到某个类上的&#xff0c; 但是存在两个问题: 使用外部包里的类, 没办法添加类注解⼀个类, 需要多个对象, ⽐如多个数据源 这种场景, 我们就需要使用方法注解 Bean 我们先来看方法注解如何使用: public c…

YOLOv3没有比这详细的了吧

YOLOv3&#xff1a;目标检测基于YOLOv2的改进 在目标检测领域&#xff0c;YOLO&#xff08;You Only Look Once&#xff09;系列以其出色的性能和速度而闻名。YOLOv3作为该系列的第三个版本&#xff0c;不仅继承了前身YOLOv2的优势&#xff0c;还在多个方面进行了创新和改进。…

Linux中的高级IO函数(一)pipe socketpair dup

Linux提供了很多高级的I/O函数。它们并不像Linux基础I/O函数&#xff08;比如open和read&#xff09;那么常用&#xff08;编写内核模块时一般要实现这些I/O函数&#xff09;&#xff0c;但在特定的条件下却表现出优秀的性能。这些函数大致分为三类&#xff1a; 用于创建文件描…

HarmonyOS开发案例:【闹钟】

介绍 使用后台代理提醒&#xff0c;实现一个简易闹钟。要求完成以下功能&#xff1a; 展示指针表盘或数字时间。添加、修改和删除闹钟。展示闹钟列表&#xff0c;并可打开和关闭单个闹钟。闹钟到设定的时间后弹出提醒。将闹钟的定时数据保存到轻量级数据库。 相关概念 [Canva…

数据结构入门——排序(代码实现)(下)

int GetMidi(int* a, int left, int right) {int mid (left right) / 2;// left mid rightif (a[left] < a[mid]){if (a[mid] < a[right]){return mid;}else if (a[left] > a[right]) // mid是最大值{return left;}else{return right;}}else // a[left] > a[mid…

MySQL-----多表查询(一)

目录 一.多表关系&#xff1a; 1.1 一对多(多对一)&#xff1a; 1.2 多对多: 1.3 一对一: 二.多表查询概述&#xff1a; 三.连接查询&#xff1a; 3.1内连接&#xff1a; 3.2外连接&#xff1a; 3.3自连接查询&#xff1a; 3.4联合查询&#xff1a; 一.多表关系&…

测试的分类(3)

目录 按照测试阶段测试 系统测试 冒烟测试和回归测试的区别 验收测试 单元测试, 集成测试, 系统测试, 回归测试之间的关系 是否按手工进行测试 手工测试 自动化测试 自动化测试和手工测试的优缺点 自动化测试优点 自动化测试缺点 手工测试优点 手工测试缺点 按照…

鸿蒙HarmonyOS应用 - ArkUI组件

ArkUI组件 基础组件 Image 声明Image组件并设置图片源 网络权限&#xff1a;ohos.permission.INTERNET Image(scr: string | PixelMap | Resource)// 1. string&#xff1a;用于加载网络图片&#xff0c;需要申请网络权限 Image("https://xxx.png")// 2. PixelMap…

快递物流订阅推送API接口如何对接

快递物流订阅推送API接口指的是订阅国内物流快递信息&#xff0c;当运单状态发生变化时&#xff0c;会推送到您的回调地址&#xff0c;直到这些运单号生命周期结束。简单点说就是先订阅快递单号再推送物流信息。那么快递物流订阅推送API接口该如何对接呢&#xff1f; 首先我们…

JVM学习笔记(四)类加载与字节码技术

目录 一、类文件结构 二、字节码指令 2.3 图解方法执行流程 1&#xff09;原始 java 代码 2&#xff09;编译后的字节码文件 3&#xff09;常量池载入运行时常量池 4&#xff09;方法字节码载入方法区 5&#xff09;main 线程开始运行&#xff0c;分配栈帧内存 6&…

道路检测车理想伴侣,国产高智能道路病害识别系统,可灵活兼容行车记录仪、无人机等数据源!

什么是视觉AI&#xff1f;通俗地说&#xff0c;视觉AI是机器代替人眼来做测量和判断&#xff0c;例如博雅仔为大家介绍的易模真人手办定制项目是基于公司独有的AI将拍摄到的实际影像“翻译”“制作”成数字3D模型&#xff0c;再经过3D打印固化成纪念手办送到用户朋友们的手上。…

将Python机器学习模型集成到C++ Qt客户端应用程序中|Qt调用python详解

0、前言 有几个不同的选项可以将你的Python机器学习模型集成到你的C Qt客户端应用程序中。以下是一些可能的解决方案&#xff1a; 创建API&#xff1a; 将你的机器学习模型部署为一个API服务。你可以使用像Flask这样的轻量级Web框架来创建一个简单的HTTP服务。这样&#xff0…

如何在一台服务器上同时运行搭载JDK 8, JDK 17, 和 JDK 21的项目:终极指南

&#x1f42f; 如何在一台服务器上同时运行搭载JDK 8, JDK 17, 和 JDK 21的项目&#xff1a;终极指南 &#x1f680; 摘要 在企业开发环境中&#xff0c;常常需要在同一台服务器上运行使用不同Java开发工具包&#xff08;JDK&#xff09;版本的多个项目。本文详细介绍如何在L…

华为鸿蒙应用--封装通用标题栏:CommonTitleBar(鸿蒙工具)-ArkTs

0、效果图 自定义通用标题栏 支持左、中、右常规标题栏设置&#xff1b; 支持自定义视图&#xff1b; 支持搜索功能 一、CommTitleBar代码 import router from ohos.router; import { Constants } from ../../constants/Constants; import { StyleConstants } from ../../…

JavaEE 初阶篇-深入了解 UDP 通信与 TCP 通信(综合案例:实现 TCP 通信群聊)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 UDP 通信 1.1 DatagramSocket 类 1.2 DatagramPacket 类 1.3 实现 UDP 通信&#xff08;一发一收&#xff09; 1.3.1 客户端的开发 1.3.2 服务端的开发 1.4 实现 …