常见的线程安全问题及解决

1. 什么是线程安全

线程安全指的是当多个线程同时访问一个共享的资源时,不会出现不确定的结果。这意味着无论并发线程的调度顺序如何,程序都能够按照设计的预期来运行,而不会产生竞态条件(race condition)或其他并发问题。

请看如下代码:

我们用两个线程分别让count++ 5w次,最后我们打印count,理论得到的结果是10w

public class ThreadDemo13 {public static int count = 0;public static void main(String[] args) throws InterruptedException {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++  是由三个cpu指令来完成的

  • load 从内存中读取数据到cpu寄存器
  • add 把寄存器中的值 +1
  • save 把寄存器中的值写回到内存中

 由于线程之间是并发执行的,每个线程执行到任何一条指令后,都可能从cpu上调度走,而且去执行其他线程,于是当 t1 线程和 t2 线程并发执行时,会存在以下情况。

可以看到 这种情况两次  count++ 实际上只让count+1了,并且实际情况中  t1 的load和add之间又能有更多的指令,那样则会导致 多个count++ 只会令count+1 ,所以导致了结果与预期不符合 

这类问题就称为线程不安全问题。

2. 线程安全问题及解决 

从上面的示例我们可以发现,导致线程不安全的原因是,count++ 这三个指令不是整体执行的,于是我们要解决这个问题就可以想办法使这三个指令为一个整体

2.1 锁 

在Java中我们可以通过加锁的方式来保证线程安全,锁具有 “互斥” “排他” 的特性

在Java中,加锁的方式有很多种,最主要的方式,是通过 synchronized 关键字

语法:

synchronized(锁对象) {//要加锁的代码
}

加锁的时候,需要“锁对象”,如果一个线程用一个锁对象加上锁以后,其他线程也尝试用这个锁对象来加锁,就会产生阻塞(BLOCKED),直到前一个对象释放锁

锁对象是一个Object对象

我们给上述ThreadDemo13中 t1, t2 中的count++加上锁:

public class ThreadDemo13 {public static int count = 0;public static void main(String[] args) throws InterruptedException {//随便创建一个对象,作为锁对象,因为所有类默认继承于Object类//所以任意一个类的对象都可以作为锁对象Object locker = new Object();Thread t1 = new Thread(() -> {for(int i = 0; i < 50000; i++) {synchronized (locker) {count++;}}});Thread t2 = new Thread(() -> {for(int i = 0; i < 50000; i++) {synchronized (locker) {count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + count);}
}

此时我们再次运行代码:

发现结果正确, 这是因为我们给  t1, t2 中的count都加上了同一个锁,运行代码时, t1 中的count++ 没有执行完时,t2中的 count++ 拿不到锁,就不会执行,同理,t2 中的count++ 没有执行完时,t1中的 count++ 也拿不到锁,也就不会执行。所以就保证了线程安全。

这里我们可以这样理解加锁操作:给代码加锁,就是规定这段代码必须拿到对应的锁才能执行,如果另一个线程中也有代码加了这把锁(即相同的锁对象),同样这段代码也必须拿到这个锁才能执行,但是这个锁只有一个,所以同一时间只能执行一段代码,另一段代码只能等上一段代码执行完,把锁释放了,才能拿到锁进而执行

注意:

  • 加了锁的代码,任然可能中途被调度出cpu,只不过调度出cpu后,任然是加锁状态
  • 只有以同一个锁对象加锁的线程间会产生锁竞争 

 synchronized 还有几种写法

1. 下面这个代码是否是线程安全的?

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

注意this指的是当前对象,我们发现 调用add的都是t,所以, t1, t2 中 都是通过 t 来加锁,所以存在锁竞争,这段代码是线程安全的:

2.  锁可以加在方法上面

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

这种写法与上面效果相同,都是通过当前对象加锁

2.2  synchronized 特性

下列代码能否正常打印Ting?

public class ThreadDemo16 {public static void main(String[] args) {Object locker = new Object();Thread t = new Thread(() -> {synchronized (locker) { //1synchronized (locker) {  //2System.out.println("Ting");}// 3}// 4});t.start();}
}

答案是可以的:

 解释:在1位置,第一次使用locker加锁,很明显这里是可以加上的,在2位置,这里也在尝试使用locker进行加锁,按照我们上面的理解,这里的锁是加不上的,但是最后却输出了Ting,这里是因为这两次加锁是同一个线程在进行,这种操作是允许的,这个特性称为可重入,这是Java开发者为了防止出现死锁而设计的,

注意:这种写法在1位置才会加锁,在2 位置时,不会真的加锁,在3位置也不会释放锁,在4位置才会释放锁 ,对于可重入锁,内部会有一个加锁次数的计数器,当加锁时计数器为0 才会加锁,每“加一次锁”计数器+1,而每出一个“}”计数器-1,为0时才释放锁

2.3 死锁的三种典型场景 

1. 一个线程,一把锁

如同上面所讲的,如果锁是不可重入锁,并且一个线程,用这把锁加锁两次就会出现死锁

2. 两个线程 两把锁

线程 1 获取到锁 A
线程 2 获取到锁 B
在这种情况下 ,1尝试获取 B, 2尝试获取 A

示例代码 :
 

public class ThreadDemo17 {public static void main(String[] args) {Object A = new Object();Object B = new Object();Thread t1 = new Thread(() -> {synchronized (A) {try {//等 t2 拿到BThread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (B) {System.out.println("t1拿到了两把锁");}}});Thread t2 = new Thread(() -> {synchronized (B) {try {//等 t1 拿到AThread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (A) {System.out.println("t2拿到了两把锁");}}});t1.start();t2.start();}
}

3. N个线程M把锁

类似于上面的两个线程两把锁的问题,

下面简单画个图演示这种情况:

这种情况下,每个线程都在等待左边的锁被释放,形成一个死锁 

解决:对每把锁都进行编号,规定每个线程都必须先获取编号小的锁,再获取编号大的锁,于是,线程1在获取到锁A前是不会获取锁F的,所以就避免了上述情况。

2.4 内存可见性引起的线程安全问题 

在Java中,多线程共享内存可能会导致内存可见性问题,从而引起线程安全问题。简单来说,内存可见性是指:当一个线程修改了共享变量的值时,这个新值可能不会立即被其他线程所看到

示例代码:

public class ThreadDemo18 {public static int flag = 0;public static void main(String[] args) {int a = 0;Thread t1 = new Thread(() -> {while(flag == 0) {}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("把flag置为1");flag = 1;});t1.start();t2.start();}
}

当我们运行代码发现:

flag被置为1后 t1线程并没有结束 

 这里我们主要查看这段代码:

这段代码的核心指令只有两条
1. 读取内存中flag的值到cpu寄存器里
2. 拿寄存器里的值和 0 比较

在上述循环中的循环速度是非常快的, 一秒钟可能就运行了几亿次,在这个执行过程中,1操作每次读取的结果都是一样的,并且我们知道,内存的读写速度,相对于2操作的比较速度,是慢得多的,在这个循环中,九成九的时间都在执行1操作,并且运行了很多次(几亿甚至上百亿),读取的值都没有变化,此时JVM 就可能做出优化:不再执行 1 操作,直接用之前寄存器中的值和0做比较。当后面 flag的值变为1后,t1 线程中寄存器中存的值还是0,所以循环不会结束。

我们可以在while中加一个sleep,让循环变慢:

public class ThreadDemo18 {public static int flag = 0;public static void main(String[] args) {int a = 0;Thread t1 = new Thread(() -> {while(flag == 0) {try {Thread.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("把flag置为1");flag = 1;});t1.start();t2.start();}
}

运行代码:

我们发现循环可以正常结束。 

这是因为不加sleep时,一秒钟循环几亿次,操作 1 的整体开销占比是非常大的,优化的迫切程度就更高;加了sleep后一秒循环1000次,操作 1 的整体开销占比就小很多了,优化的迫切程度也就每那么高。

Java中提供了 volatile 关键字,可以使上述优化被关闭 

public class ThreadDemo18 {volatile public static int flag = 0;public static void main(String[] args) {int a = 0;Thread t1 = new Thread(() -> {while(flag == 0) {}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("把flag置为1");flag = 1;});t1.start();t2.start();}
}

运行结果:

volatile 有两个功能 

1. 保证内存可见性
2. 防止指令重排序

2.5 线程饿死 

 Java中的线程饥饿(Thread Starvation),是指某个或某些线程无法获取到所需的CPU时间或其它系统资源,从而陷入长时间的等待状态,无法继续正常执行。这种情况可能会导致程序性能下降、响应时间延长,甚至出现死锁等严重问题。

线程饥饿通常是由于以下几个原因引起的:

  1. CPU资源被占用:当有一个或多个线程占用了大量的CPU资源时,其他线程可能无法获得足够的CPU时间,从而无法正常执行。

  2. 长时间等待资源:当某个线程需要等待某个资源(如锁、I/O操作等)时,如果该资源一直被其他线程占用,那么该线程可能会长时间等待,从而导致线程饥饿。
    示例代码:

    public class ThreadDemo19 {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(() -> {synchronized (locker) {while(true) {//模拟长时间占用锁}}});Thread t2 = new Thread(() -> {synchronized (locker) {System.out.println("执行了t2");}});t1.start();//确保t1先拿到lockerThread.sleep(100);t2.start();}
    }

  3. 线程优先级不足:当有多个线程同时竞争某个资源时,如果优先级较低的线程一直无法获得该资源,那么它们可能会陷入长时间的等待状态。
    例如:现有 1,2,3 三个线程,线程1 的优先级更高,现在线程1 拿到了锁,但是现在某个条件不满足,线程1无法执行,所以线程1 由把锁释放了,但是线程 1 释放锁之后 任然会参与到锁竞争中,又由于 线程1的优先级高于线程2和线程3,所以任然是线程1拿到锁,于是导致了死锁。这种情况下我们可以使用 wait/notify 解决 让线程1 在条件满足时 再尝试获取锁

2.6 wait / notify 

  1. wait()方法

    • wait()方法是Object类中定义的方法,可以在任何对象上被调用。它使当前线程释放对象的锁,并让线程进入等待状态,直到其他线程调用相同对象上的notify()或notifyAll()方法将其唤醒。
    • 调用wait()方法会导致当前线程进入等待队列,并释放对象的监视器锁(即释放synchronized块或方法中的锁),允许其他线程获得该锁并执行相应操作。
    • wait()方法可以指定等待的超时时间,如果在指定的时间内没有被唤醒,则线程会自动苏醒。
  2. notify()方法

    • notify()方法也是Object类中定义的方法,用于唤醒等待在相同对象上的某个线程。它会选择性地通知等待队列中的一个线程,表示该线程可以尝试重新获得对象的锁。
    • 如果有多个线程在等待相同对象上的锁,那么只有其中一个线程会被唤醒,具体唤醒哪个线程是不确定的。
    • notifyAll()方法则会唤醒等待队列中的所有线程。

 示例:

public class ThreadDemo20 {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(() -> {synchronized (locker) {try {System.out.println("wait之前");//wait必须在synchronized内部,因为要释放锁的前提是得加上锁locker.wait();System.out.println("wait之后");} catch (InterruptedException e) {//wait 和 sleep join都是一类的可能会被提前唤醒,需要捕获异常e.printStackTrace();}}});Thread t2 = new Thread(() -> {synchronized (locker) {System.out.println("notify之前");//Java特别规定notify也必须在synchronized内部locker.notify();System.out.println("notify之后");}});t1.start();Thread.sleep(1000);t2.start();}
}

 执行过程:

t1 执行后会立刻拿到锁,并且打印 “wait之前” 然后进入wait方法 (释放锁,阻塞等待),然后等待1秒,t2开始执行,拿到锁 打印 “notify”之前 ,然后执行 notify,让 t1 停止阻塞,重新参与锁竞争

注意:wait()可以设置最大等待时间,具体规则和join相同

 

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

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

相关文章

绘制颜色矩的直方图

# 代码5-2 绘制颜色矩的直方图 def color_moments(img, trans_hsvFalse):if trans_hsv True:img cv2.cvtColor(img, cv2.COLOR_BGR2HSV)# 颜色分割f, s, t cv2.split(img)# 创建特征存放列表color_feature []# 一阶f_mean np.mean(f)s_mean np.mean(s)t_mean np.mean(t)…

wordpress路径怎么优化?wordpress伪静态怎么做?

Wordpress这个程序是动态的&#xff0c;在后台中设置链接的格式为朴素&#xff0c;就可以了&#xff0c;这样简单又方便&#xff0c;因为百度对于路径的都是一样对待的&#xff0c;静态路径和动态路径&#xff0c;都是一样的对待。 有的时候&#xff0c;有的人会认为动态路径不…

vue+less+style-resources-loader 配置全局颜色变量

全局统一样式后&#xff0c;可配置vue.config.js实现全局颜色变量&#xff0c;方便在编写时使用统一风格的色彩 一、新建global.less 二、下载安装style-resources-loader npm i style-resources-loader --save-dev三、在vue.config.js中进行配置 module.exports {pluginOpt…

Python Locals:引领代码风潮,变量管理新尝试

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 在Python中&#xff0c;locals()函数是一个强大的工具&#xff0c;它使程序员能够访问和操作当前作用域内的局部变量。本文将深入探讨locals()函数的功能、应用和重要性。 动态变量赋值和操作 locals()函数让我…

算法通关村第七关—理解二叉树的遍历(白银)

深入理解前中后序遍历 给定一棵二叉树 二叉树前序遍历 public void preorder(TreeNode root,List<Integer>res){if&#xff08;rootnull){return;}res.add(root.val);preorder(root.left,res);preorder(root.right,res); }递归的过程如下图所示 从图中可以看到&#x…

JavaScript编程基础 – For循环

JavaScript编程基础 – For循环 JavaScript Programming Essentials – For Loop By JacksonML 循环可以多次执行代码块&#xff0c;而不用反复重写相同的语句。这无疑对提升代码质量、减少错误大有脾益。本文将简要介绍for循环的几种案例&#xff0c;希望对读者有所帮助。 …

【Python篇】文件概述 | 读文件 | 写文件 | 追加文件操作

文章目录 &#x1f339;什么是文件&#x1f6f8;读 操作 — r⭐打开文件⭐读取文件&#x1f388;循环读取&#x1f388;读取文件中某一个词语的个数 ⭐关闭文件 &#x1f33a;小结&#x1f6f8;写 操作 — w&#x1f6f8;追加 操作 — a &#x1f339;什么是文件 文件是计算机…

『亚马逊云科技产品测评』活动征文|基于亚马逊云EC2搭建OA系统

授权声明&#xff1a;本篇文章授权活动官方亚马逊云科技文章转发、改写权&#xff0c;包括不限于在 Developer Centre, 知乎&#xff0c;自媒体平台&#xff0c;第三方开发者媒体等亚马逊云科技官方渠道 亚马逊EC2云服务器&#xff08;Elastic Compute Cloud&#xff09;是亚马…

2023年AI报告:首个投研GPTs测评重塑AI竞争格局

今天分享的是AI系列深度研究报告&#xff1a;《2023年AI报告&#xff1a;首个投研GPTs测评重塑AI竞争格局》。 &#xff08;报告出品方&#xff1a;国盛证券&#xff09; 报告共计&#xff1a;10页 1.一键创建 GPTs 助力行业研究 GPTs 目前仅对企业用户和 ChatGPT Plus 会员…

右值引用和移动语句(C++11)

左值引用和右值引用 回顾引用 我们之前就了解到了左值引用&#xff0c;首先我们要了解引用在编译器底层其实就是指针。具体来说&#xff0c;当声明引用时&#xff0c;编译器会在底层生成一个指针来表示引用&#xff0c;但在代码编写和使用时&#xff0c;我们可以像使用变量类…

HarmonyOS系统和Android系统有什么区别?

鸿蒙系统和安卓系统有如下几点区别&#xff1a;点击这里查看获取鸿蒙系统资料方式 (qq.com) 一、开发商不同&#xff1a; 鸿蒙OS&#xff1a;由中国华为公司主导开发的系统&#xff0c;2019年首次发布&#xff0c;现在已经更新至鸿蒙OS4.0。 安卓系统&#xff1a;是由安迪鲁宾…

visual Studio MFC 平台实现图像增强中的线性变换(负变换)和非线性变换(对数与幂律)

MFC 实现数字图像处理中的图像增强操作 本文使用visual Studio MFC 平台实现图像增强中典型的三种图像增强的方法中的两大类&#xff0c;包括线性变换–>负变换&#xff0c;非线性变换–>对数变换和幂律变换&#xff1b;其中第三大类分段式变换可以参考MFC实现图像增强–…

Android Termux 安装Kali Linux 或 kali Nethunter史诗级详细教程

Android Termux 安装Kali Linux 或 kali Nethunter史诗级详细教程 一、Termux配置1、下载安装2、配置存储和换源3、基本工具安装 二、Kali Linux安装1、下载安装脚本2、更换apt源3、图形化安装 三、Kali Nethunter安装1、下载安装脚本2、更换apt源3、图形化连接 四、报错汇总1、…

2023年5月电子学会青少年软件编程 Python编程等级考试一级真题解析(判断题)

2023年5月Python编程等级考试一级真题解析 判断题(共10题,每题2分,共20分) 26、在编写较长的Python程序时,所有代码都不需要缩进,Python会自动识别代码之间的关系 答案:错 考点分析:考查python代码书写格式规范,python编写较长的程序时,需要明确严格的缩进,不然有…

【ArcGIS Pro微课1000例】0044:深度学习--面部模糊(马赛克)

本文讲解ArcGIS Pro中通过深度学习工具实现人脸面部模糊,起到马赛克的作用。 文章目录 一、效果对比二、工具介绍三、案例实现一、效果对比 原始图片: 深度学习后的模糊照片: 二、工具介绍 本工具为ArcGIS Pro工具箱中的深度学习工具中的:使用深度学习分类像素,如下所示…

vue3中自定义hook函数

使用Vue3的组合API封装的可复用的功能函数 自定义hook的作用类似于vue2中的mixin技术 自定义Hook的优势: 很清楚复用功能代码的来源, 更清楚易懂 案例: 收集用户鼠标点击的页面坐标 hooks/useMousePosition.ts文件代码&#xff1a; import { ref, onMounted, onUnmounted …

Java LeetCode篇-深入了解关于栈的经典解法(栈实现:中缀表达式转后缀)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 中缀表达式转后缀说明 1.1 实现中缀表达式转后缀思路 2.0 逆波兰表达式求值 2.1 实现逆波兰表达式求值思路 3.0 有效的括号 3.1 实现有效的括号思路 4.0 栈的压…

法学毕业生个人简历16篇

想要从众多法学毕业求职者中脱颖而出&#xff0c;找到心仪的相关工作&#xff1f;可以参考这16篇精选的法学专业应聘简历案例&#xff0c;无论是应届比预算还是有工作经验&#xff0c;都能从中汲取灵感&#xff0c;提升简历质量。希望对大家有所帮助。 法学毕业生简历模板下载…

RPG项目01_脚本代码

基于“RPG项目01_场景及人物动画管理器”&#xff0c;我们创建一个XML文档 在资源文件夹下创建一个文件夹&#xff0c; 命名为Xml 将Xnl文档拖拽至文件夹中&#xff0c; 再在文件夹的Manager下新建脚本LoadManager 写代码&#xff1a; using System.Collections; using System…

Pycharm调用Conda虚拟环境

参考这个链接的评论区回答&#xff1a;Pycharm调用Conda虚拟环境 笑死&#xff0c;我之前也是这样的&#xff0c;不过好像也能用&#xff0c;搞不懂~