Java 多线程系列Ⅱ(线程安全)

线程安全

  • 一、线程不安全
    • 线程不安全的原因:
  • 二、线程不安全案例与解决方案
    • 1、修改共享资源
      • synchronized 使用
      • synchronized 特性
    • 2、内存可见性
      • Java内存模型(JMM)
      • 内存可见性问题
    • 3、指令重排列
    • 4、synchronized 和 volatile
    • 5、拓展知识:修饰符顺序规范

一、线程不安全

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。否则就称之为线程不安全。

线程不安全的原因:

  1. 抢占式执行,可以说“线程的无序调度”是罪魁祸首,万恶之源!!!(是操作系统内核来实现的,程序员无法控制)
  2. 多个线程修改同一个变量。
  3. 修改操作,不是原子(不可分割的最小单位)的。某个操作对应单个cpu指令就是原子的,如果单个操作对应多个CPU指令,大概率不是原子的。正是因为不是原子的,导致多个线程的指令排列存在更多的变数。
  4. 内存可见性,引起的线程不安全。
  5. 指令重排列,引起的线程不安全。

二、线程不安全案例与解决方案

1、修改共享资源

即针对于多个线程修改同一个变量的情况,由于修改操作可能不是原子的(单条cpu指令),在多线程的随机调度下,就会导致多个线程的指令排列存在更多变数。

例如如下代码:

class Counter {private int count = 0;public void add() {count++;}public int getCount() {return count;}
}public class ThreadExample_unsafe {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.add();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.add();}});t1.start();t2.start();//等两个线程结束后查看结果t1.join();t2.join();System.out.println(counter.getCount());}
}

结果分析:
对于如上代码,两个线程 t1、t2 各自对 count 自增 50000 次,理论情况下结果应为100000,但是实际运行结果小于100000,尽管多次运行依旧如此。以上现象正是因为,在 t1、t2 两个线程修改 count 时,由于每个 ++ 操作都不是原子的,可以分割为(1.读取 2.修改 3.写入),在系统随机调度的加持下就会导致 t1、t2 线程++操作实际指令排列顺序有多种可能,最终导致结果异常。如下图绘制了两种可能出现的情况:

解决方案-加锁

对于以上场景,在保证并发执行的情况下,由于线程的随机调度是系统内核来实现的,程序员不可控,而多个线程修改同一变量又是业务需求,所以要保证该场景下的线程安全我们可以考虑将修改操作变成原子的。而“加锁”可以保证原子性效果synchronized 是 Java 中用于实现锁的关键字,下面我们详细介绍:

synchronized 使用

Java中使用synchronized针对“对象头”加锁,synchronized 势必要搭配一个具体的对象来使用

(1)synchronized对普通方法加锁

// 给实例方法加锁
public void add() {synchronized (this) {count++;}
}//如果直接给方法使用synchronized修饰,此时就相当于以this为锁对象
synchronized public void add() {count++;
}

(2)synchronized对静态方法加锁

//给静态方法加锁
public static void test2() {// Counter.class相当于类对象synchronized (Counter.class) {}
}
//如果直接给方法使用synchronized修饰,此时就相当于以Counter.class为锁对象
synchronized public static void test() {}

(3)synchronized对任意代码块加锁

// 自定义锁对象
Object locker = new Object();synchronized (locker) {// 代码逻辑// . . .
}

拓展:被 synchronized 修饰的方法又叫同步方法;被 synchronized 修饰的代码块又叫同步代码块。

synchronized 特性

  1. 进入 synchronized 修饰的代码块, 相当于 加锁。 退出 synchronized 修饰的代码块, 相当于 解锁。
  2. synchronized修饰的代码块具有原子性效果。即加锁是让多个线程的某个部分进行串行。
  3. synchronized()其中()里的对象,可以是任意一个Object对象,这个对象也被称为锁对象。synchronized用的锁是存在Java对象头里的,可以粗略理解成:每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态,如果当前是 “解锁” 状态, 那么就可以使用, 使用时需要设为 “加锁” 状态,如果当前是 “加锁” 状态, 那么其他线程无法使用, 只能阻塞等待
  4. synchronized是互斥锁,所谓互斥,即同一时间多个线程不能对同一对象加锁。而是同一时刻只能有一个线程获取锁,其他线程阻塞等待。因此多个线程尝试对同一个锁对象加锁,此时就会产生锁竞争,针对不同对象加锁,就不会有锁竞争。
  5. 阻塞等待:针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁。
  6. 获取锁原则:上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”。 这也就是操作系统线程调度的一部分工作.。假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则。
  7. 拓展:synchronized 既是悲观锁,也是乐观锁。既是轻量级锁,也是重量级锁。轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁实现。是互斥锁不是读写锁,是非公平锁。(后续介绍)

2、内存可见性

Java内存模型(JMM)

介绍内存可见性之前,我们先简单了解一下java内存模型:

  • 工作内存-work memory :CPU寄存器 + 缓存
  • 主内存-main memory :内存
  1. 线程之间的共享变量存在 主内存 (Main Memory).
  2. 每一个线程都有自己的 “工作内存” (Working Memory) .
  3. 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
  4. 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.

为什么引入工作内存?

这里引入工作内存主要是因为CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度。在某些情况下,这也是提高效率的一种重要手段。比如某个代码中要连续 10000 次读取某个变量的值, 如果 10000 次都从内存读, 速度是很慢的。但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9999 次读数据就不必直接访问内存了。效率就大大提高了。

内存可见性问题

内存可见性是指当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值。

什么是内存可见性引起的多线程安全问题?

一般来说由内存可见性引发的多线程问题,是由于编译器的优化。例如:

public class ThreadExample_unsafe2 {public static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while (flag == 0) {//空转}System.out.println("循环结束,t1结束!");});Thread t2 = new Thread(()->{Scanner scanner = new Scanner(System.in);System.out.print("请输入一个整数:");flag = scanner.nextInt();});t1.start();t2.start();}
}

结果分析
如上代码,t1线程中flag == 0涉及到两个CPU指令,假设这两个指令分别是load-从内存读取数据到工作内存(CPU寄存器),cmp-比较寄存器中的值是否为0。对于这两个操作,load的时间开销远远高于cmp。此时编译器在处理的时候发现,load的开销很大,每次load的结果都一样,此时编译器就做了一个非常大胆的决定,即只有第一次load执行从内存读取到工作内存,后续循环的load直接从工作内存读取。所以尽管输入了不为0的整数,因为工作内存数据不变,程序依然继续运行。

关于编译器优化:

针对以上线程安全问题,是编译器优化的结果,关于编译器优化,这是一个很普遍的事,编译器优化就是能够智能调整你代码的执行逻辑,保证程序结果不变的情况下,通过加减语句,通过语句变换等一系列操作,让整个程序的执行效率大大提升。但是对于编译器优化在单线程情况下一般是不会出现任何问题的,但是多线程下不能保证。

解决方案

使用volatile修饰:被关键字volatile修饰的变量,此时编译器就会禁止例如上述优化,能够保证每次都是从内存重新读取数据到工作内存,保证了内存可见性。

3、指令重排列

指令重排,也是程序优化的一种手段,和编译器的优化有直接的关系,也和线程不安全直接相关。如果是单线程的情况下,这样的调整没问题,但是在多线程的情况下就会发生线程安全问题。

例如下面伪代码:

其中线程t1中s = new Student();大体可以分为3步:

  1. 申请内存空间
  2. 调用构造方法(初始化内存数据)
  3. 把对象的引用赋值给s(内存地址的赋值)

如果是单线程下,上述操作很容易保证,如果是多线程下,指令2,3重排先执行3后执行2,在刚执行完指令3后,t2线程执行s.learn();就会出现bug。

解决方案

  1. 当前场景下可使用volatile修饰,因为volatile具有防止指令重拍的作用,可以解决上述可能出现的问题。
  2. 可以对new操作加锁-synchronized

4、synchronized 和 volatile

  1. synchronized 保证原子性,volatile 不保证原子性。
  2. 一般情况下 volatile 适用于一个线程读一个线程写的情况。
  3. 一般情况下 synchronized 适用于多个线程写的情况。

5、拓展知识:修饰符顺序规范

在Java中,修饰符的顺序可以任意排列,但是为了方便阅读和代码的一致性,一般会按照以下的顺序进行排列:

  1. 可见性修饰符(public, protected, private)
  2. 非可见性修饰符(static, final, abstract)
  3. 类型修饰符(class, interface, enum)
  4. 其他修饰符(synchronized, transient, volatile,native, strictfp)

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

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

相关文章

【网络安全带你练爬虫-100练】第17练:分割字符串

目录 一、目标1&#xff1a;使用函数分割 二、目标2&#xff1a;使用函数模块 三、目标3&#xff1a;使用正则匹配 一、目标1&#xff1a;使用函数分割 目标&#xff1a;x.x.x.x[中国北京 xx云] 方法&#xff1a;split函数replace函数 1、分割&#xff1a;使用split()方法将…

iPhone 15预售:获取关键信息

既然苹果公司将于9月12日正式举办iPhone 15发布会,我们了解所有新机型只是时间问题。如果你是苹果的狂热粉丝,或者只是一个早期用户,那么活动结束后,你会想把所有的注意力都集中在iPhone 15的预购上——这样你就可以保证自己在发布日会有一款机型。 有很多理由对今年的iPh…

聊天平台Revolt的搭建

经网友 凌尘 提醒&#xff0c;Web-Check 最新的镜像版本&#xff0c;容器端口已经从 8888 改为了 3000&#xff0c;特此更正&#xff01; 什么是 Revolt &#xff1f; Revolt 是一个开源的用户至上的聊天平台。是在不牺牲任何可用性的情况下与朋友和社区保持联系的最佳方式之一…

从零学算法(剑指 Offer 36)

123.输入一棵二叉搜索树&#xff0c;将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点&#xff0c;只能调整树中节点指针的指向。 为了让您更好地理解问题&#xff0c;以下面的二叉搜索树为例&#xff1a; 我们希望将这个二叉搜索树转化为双向循环链表。…

pxe网络装机

目录 PXE是什么&#xff1f; PXE的组件&#xff1a; 配置vsftpd关闭防火墙与selinux ​编辑配置tftp 准备pxelinx.0文件、引导文件、内核文件 ​编辑配置dhcp 创建default文件 创建新虚拟机等待安装&#xff08;交互式安装完毕&#xff09; 创建客户端验证&#xff08;…

完整开发实现公众号主动消息推送,精彩内容即刻到达

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌&#xff0c;CSDN博客专家&#xff0c;阿里云社区专家博主&#xff0c;2023年6月CSDN上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师…

使用 zipfile创建文件压缩工具

在本篇博客中&#xff0c;我们将使用 wxPython 模块创建一个简单的文件压缩工具。该工具具有图形用户界面&#xff08;GUI&#xff09;&#xff0c;可以选择源文件夹中的文件&#xff0c;将其压缩为 ZIP 文件&#xff0c;并将压缩文件保存到目标文件夹中。 C:\pythoncode\new\z…

python基础之miniConda管理器

一、介绍 MiniConda 是一个轻量级的 Conda 版本&#xff0c;它是 Conda 的精简版&#xff0c;专注于提供基本的环境管理功能。Conda 是一个流行的开源包管理系统和环境管理器&#xff0c;用于在不同的操作系统上安装、管理和运行软件包。 与完整版的 Anaconda 相比&#xff0c…

【力扣每日一题】2023.8.31 一个图中连通三元组的最小度数

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目给我们一个无向图&#xff0c;要我们找出三个节点&#xff0c;这三个节点他们两两相连&#xff0c;这三个节点除了连接到对方的其他线…

【重要】Nand Flash基础知识与坏块管理机制的介绍

概述 Flash名称的由来&#xff0c;Flash的擦除操作是以block块为单位的&#xff0c;与此相对应的是其他很多存储设备&#xff0c;是以bit位为最小读取/写入的单位&#xff0c;Flash是一次性地擦除整个块&#xff1a;在发送一个擦除命令后&#xff0c;一次性地将一个block&…

vue之若依分页组件的导入使用(不直接使用若依框架,只使用若依分页组件)

vue之若依分页组件的导入使用 步骤 步骤&#xff1a; 工具类&#xff1a;src/utils/scroll-to.js 样式&#xff1a;src/assets/styles/ruoyi.scss 组件&#xff1a;src/components/Pagination 全局挂载&#xff1a;src/main.js 复制工具类 复制若依框架中的src/utils/scrol…

Shell编程之函数

目录 基本概念 自定义函数 系统函数 1.read 2.basename 3.dirname 基本概念 将一段代码组合封装在一起实现某个特定的功能或返回某个特定的值&#xff0c;然后给这段代码取个名字&#xff0c;也就是函数名&#xff0c;在需要实现某个特定功能的时候直接调用函数名即可。 函…

【FPGA】verilog语法的学习与应用 —— 位操作 | 参数化设计

【FPGA】verilog语法的学习与应用 —— 位操作 | 参数化设计 学习新语法&#xff0c;争做新青年 计数器实验升级&#xff0c;让8个LED灯每个0.5s的速率循环闪烁&#xff0c;流水灯ahh好久不见~ 去年光这个就把我折磨够呛。。我肉眼可见的脱发就是从那时候开始的。。在那两个月…

目标检测后的图像上绘制边界框和标签

效果如图所示&#xff0c;有个遗憾就是CV2在图像上显示中文有点难&#xff0c;也不想用别的了&#xff0c;所以改成了英文&#xff0c;代码在下面了&#xff0c;一定要注意一点&#xff0c;就是标注文件的读取一定要根据自己的实际情况改一下&#xff0c;我的所有图像的标注文件…

java八股文面试[JVM]——双亲委派模型

1.当AppClassLoader去加载一个class时&#xff0c;它首先不会自己去尝试加载这个类&#xff0c;而是把类加载请求委托给父加载器ExtClassLoader去完成。 2.当ExtClassLoader去加载一个class时&#xff0c;它首先也不会去尝试加载这个类&#xff0c;而是把类加载请求委托给父加载…

【TI毫米波雷达笔记】SOC外设初始化配置及驱动(以IWR6843AOP为例)

【TI毫米波雷达笔记】SOC外设初始化配置及驱动&#xff08;以IWR6843AOP为例&#xff09; 最基本的工程建立好以后 需要给SOC进行初始化配置 SOC_Cfg socCfg; //SOC配置结构体Task_Params taskParams; //任务参数SOC_Handle socHandle;ESM_init(0U); …

关于Maxwell与Kafka和数据库的监控

1.Maxwell的配置 其实就是配置两端的配置信息,都要能连接上,然后才能去传输数据 config.properties #Maxwell数据发送目的地&#xff0c;可选配置有stdout|file|kafka|kinesis|pubsub|sqs|rabbitmq|redis producerkafka # 目标Kafka集群地址 kafka.bootstrap.servershadoop102…

OpenShift 4 - 用 Prometheus 和 Grafana 监视用户应用定制的观测指标(视频)

《OpenShift / RHEL / DevSecOps 汇总目录》 说明&#xff1a;本文已经在 OpenShift 4.13 的环境中验证 文章目录 OpenShift 的监控功能构成部署被监控应用用 OpenShift 内置功能监控应用用 Grafana 监控应用安装 Grafana 运行环境配置 Grafana 数据源定制监控 Dashboard 演示视…

学习JAVA打卡第四十九天

Random类 尽管可以使用math类调用static方法random&#xff08;&#xff09;返回一个0~1之间的随机数。&#xff08;包括0.0但不包括0.1&#xff09;&#xff0c;即随机数的取值范围是[0.0&#xff0c;1.0]的左闭右开区间。 例如&#xff0c;下列代码得到1&#xff5e;100之间…

网络渗透day6-面试01

&#x1f609; 和渗透测试相关的面试问题。 介绍 如果您想自学网络渗透&#xff0c;有许多在线平台和资源可以帮助您获得相关的知识和技能。以下是一些受欢迎的自学网络渗透的平台和资源&#xff1a; Hack The Box: Hack The Box&#xff08;HTB&#xff09;是一个受欢迎的平…