【多线程】线程安全 问题

线程安全 问题

  • 一. 线程不安全的典型例子
  • 二. 线程安全的概念
  • 三. 线程不安全的原因
    • 1. 线程调度的抢占式执行
    • 2. 修改共享数据
    • 3. 原子性
    • 4. 内存可见性
    • 5. 指令重排序

一. 线程不安全的典型例子

class ThreadDemo {static class Counter {public int count = 0;void increase() {count++;}}public static void main(String[] args) throws InterruptedException {final Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}
}

多次执行的结果:

在这里插入图片描述
两个线程各 加了 50000 次, 但最终结果都不是我们预期的 100000, 并且相差甚远。

二. 线程安全的概念

操作系统调度线程是随机的(抢占式),正因为这样的随机性,就可能导致程序的执行出现一些 bug。

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

三. 线程不安全的原因

1. 线程调度的抢占式执行

线程是抢占式执行,线程间的调度就充满随机性。
这是线程不安全的万恶之源,但是我们无可奈何,无法解决。

2. 修改共享数据

多个线程针对同一变量进行了修改操作,假如说多个线程针对不同变量进行修改则没事,多个线程针对相同变量进行读取也没事。

上面的线程不安全的代码中, 涉及到多个线程针对 counter.count 变量进行修改.
此时这个 counter.count 是一个多个线程都能访问到的 “共享数据”

在这里插入图片描述

counter.count 这个变量就是在堆上. 因此可以被多个线程共享访问.

3. 原子性

针对变量的操作不是原子的,通过加锁操作,可以把多个操作打包成一个原子操作。

一条 java 语句不一定是原子的,也不一定只是一条指令
比如上面的 count++,其实是由三步操作组成的:
(1)从内存把数据 count 读到 CPU
(2)进行数据更新 count = count + 1
(3)把更新后的数据 count 写回到 内存

所以说导致上面那段代码线程不安全的原因就是:

在这里插入图片描述

只要 t2 是在 t1 线程 save 之前读的, t2 的自增就会覆盖 t1 的自增, 那么两次加 1 的效果都相当于只加了 1 次.
所以上面的代码的执行结果 在 5w ~ 10w,并且大多数是靠近 5w 的。
(小于 5w 的是非常少见的, 这种情况就是 t2 线程覆盖了 t1 线程的多次 自增操作, 也就是说 t2 线程的 load 与 save 之间跨度很大的情况.)

解决:加锁 ! 打包成原子操作。
最常见的加锁方式就是 使用 synchronized

class ThreadDemo {static class Counter {public int count = 0;synchronized void increase() {count++;}}public static void main(String[] args) throws InterruptedException {final Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}
}

在这里插入图片描述

在自增之前加锁,自增之后再解锁。
当一个线程加锁成功时,其他线程再尝试加锁就会阻塞,直到占用锁的线程将锁释放。

那这样不就是串行执行了嘛?那多线程又有什么用 ?
实际开发中,一个线程要执行的任务很多,可能只有其中的很少一部分会涉及到线程安全问题,才需要加锁,而其他地方都能并发执行。

注意:
加锁,是要明确指出对哪个对象进行加锁的,如果两个线程对同一个锁对象进行加锁才会产生锁竞争(阻塞等待),如果对不同的对象进行加锁,那么不会产生锁竞争(阻塞等待)。
上面代码中 synchronized 加在方法上,那么就是对 Counter 对象加锁,对应到代码中就是两个线程就是对 counter 这个实例对象加锁。

4. 内存可见性

可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.

Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型.
目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.

在这里插入图片描述

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

实际并没有这么多 “内存”. 这只是 Java 规范中的一个术语, 是属于 “抽象” 的叫法.
所谓的 “主内存” 才是真正硬件角度的 “内存”. 而所谓的 “工作内存”, 则是指 CPU 的寄存器和高速缓存.

由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 “副本”. 此时修改线程 1 的工作内存中的值, 线程 2 的工作内存不一定会及时变化.

举一个栗子:
针对同一个变量:
一个线程进行循环读取操作,另一个线程在某个时机进行了修改操作。

在这里插入图片描述

比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的比寄存器慢 3-4 个数量级. 
但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问
内存了. 效率就大大提高了. (编译器优化的结果)

解决:

  1. 使用 synchronized
    synchronized 不仅能保证原子性,同时能保证内存可见性,
    被 synchronized 修饰的代码,编译器不会轻易优化。

  2. 使用 volatile 关键字
    volatile 和原子性无关,但是能保证内存可见性,禁止编译器优化,每次都要从内存中读取变量。

5. 指令重排序

什么是代码重排序?
举个栗子:
一段代码是这样的:

1. 去前台取下 U2. 去教室写 10 分钟作业
3. 去前台取下快递

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台,提高效率。这种叫做指令重排序。

编译器对于指令重排序的前提是 “保持逻辑不发生变化”.
这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了,
多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测,
因此激进的重排序很容易导致优化后的逻辑和之前不等价.

解决:
使用 volatile 关键字
volatile 除了能保证内存可见性之外还能防止指令重排序。

好啦! 以上就是对 线程安全 问题的讲解,希望能帮到你 !
评论区欢迎指正 !

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

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

相关文章

3D点云测量:计算三个平面的交点

文章目录 0. 测试效果1. 基本内容文章目录:3D视觉测量目录0. 测试效果 1. 基本内容 计算三个平面的交点需要找到满足所有三个平面方程的点。三个平面通常由它们的法向量和通过它们的点(或参数形式的方程)来定义。以下是计算三个平面的交点的一般步骤: 假设有三个平面,分别…

MyBatis-Plus排除不必要的字段

查询学生信息排除年龄列表 &#x1f4da;&#x1f50d; 使用MyBatis-Plus排除某些字段。如果你想要进行查询&#xff0c;但又不需要包含某些字段&#xff0c;那么这个功能将非常适合你。&#x1f50d;&#x1f393;&#x1f4dd; 1. 学生信息查询-排除年龄列表 在使用 MyBat…

【Cisco Packet Tracer】管理方式,命令,接口trunk,VLAN

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

【Redis】为什么要学 Redis

文章目录 前言一、Redis 为什么快二、Redis 的特性2.1 将数据储存到内存中2.2 可编程性2.3 可扩展性2.4 持久性2.5 支持集群2.6 高可用性 三、Redis 的应用场景四、不能使用 Redis 的场景 前言 关于为什么要学 Redis 这个问题&#xff0c;一个字就可以回答&#xff0c;那就是&…

Vue3+Ts+Vite项目(第一篇)——使用Vite创建Vue3项目

概述 保姆级详解&#xff0c;带你使用 Vite 创建 Vue3 项目&#xff0c;全程cv即可 文章目录 概述一、 安装 Vite二、 创建项目2.1 运行上述命令后&#xff0c;会让我们输入项目名称。可以写一个 vue3-study2.2 选择项目模板&#xff0c;此处选择 Vue&#xff0c;然后回车确定…

mysql课堂笔记 mac

目录 启动mac上的mysql 进入mysql mac windows 创建数据库 创建表 修改字段数据类型 修改字段名 增加字段 删除字段 启动mac上的mysql sudo /usr/local/mysql/support-files/mysql.server start 直接输入你的开机密码即可。 编辑 进入mysql mac sudo /usr/local…

深入浅出AXI协议(6)——传输属性

一、前言 在之前的文章中&#xff0c;我们介绍的主要内容是AXI协议的数据读写结构和读写响应结构&#xff0c;主要讲述了当遇到各种特殊情况时,AXI如何完成数据的读写操作&#xff0c;最后介绍了读写响应的4种类型。 在本文中&#xff0c;我们将介绍AXI协议的传输属性。 二、传…

【C++】day3学习成果:类

1.自行封装一个栈的类&#xff0c;包含私有成员属性&#xff1a;栈的数组、记录栈顶的变量 成员函数完成&#xff1a;构造函数、析构函数、拷贝构造函数、入栈、出栈、清空栈、判空、判满、获取栈顶元素、求栈的大小 头文件stack.h: #ifndef STACK_H #define STACK_H#include …

Linux常见指令

目录 前言 一、Linux下的基本指令 01. ls 指令 02. pwd 指令 03. cd 指令 04. touch 指令 05. mkdir 指令&#xff08;重要&#xff09; 06. rmdir 指令 && rm 指令&#xff08;重要&#xff09; 07. man 指令&#xff08;重要&#xff09; extra nano 08. cp 指…

并发聊天服务器编写

并发聊天服务器 package mainimport ("fmt""net""strings""time" )// 结构体 type Client struct {C chan string //用户发送数据的管道Name string //用户名Addr string //网络地址 }// 保存在线用户 cliAddr -->cli…

UNIX网络编程卷一 学习笔记 第三十一章 流

在大多数源自SVR 4的内核中&#xff0c;X/Open传输接口&#xff08;X/Open Transport Interface&#xff0c;XTI&#xff0c;是独立于套接字API的另一个网络编程API&#xff09;和网络协议通常就像终端IO系统那样也使用流系统&#xff08;STREAMS system&#xff09;实现。 我…

Selenium 隐藏浏览器指纹特征的几种方式

我们使用 Selenium 对网页进行爬虫时&#xff0c;如果不做任何处理直接进行爬取&#xff0c;会导致很多特征是暴露的 对一些做了反爬的网站&#xff0c;做了特征检测&#xff0c;用来阻止一些恶意爬虫 本篇文章将介绍几种常用的隐藏浏览器指纹特征的方式 1. 直接爬取 目标对…

基于微服务+Java+Spring Cloud +UniApp +MySql开发的智慧工地源码(物联网、人工智能、AI识别、危大工程)

智慧工地系统利用物联网、人工智能、云计算、大数据、移动互联网等新一代信息技术&#xff0c;通过工地中台、三维建模服务、视频AI分析服务等技术支撑&#xff0c;实现智慧工地高精度动态仿真&#xff0c;趋势分析、预测、模拟&#xff0c;建设智能化、标准化的智慧工地综合业…

vue3+ts项目打包后的本地访问

注意&#xff1a;打包之后不可直接点击html访问&#xff0c;需要给项目安装本地服务&#xff01; 1、安装servenpm i -g serve 2、打包项目npm run build 生成dist文件夹 3、本地访问serve dist 运行service dist之后的控制台 可复制下方的地址运行打包后的项目&#xff0c;运行…

强大的JTAG边界扫描(5):FPGA边界扫描应用

文章目录 1. 获取芯片的BSDL文件2. 硬件连接3. 边界扫描测试4. 总结 上一篇文章&#xff0c;介绍了基于STM32F103的JTAG边界扫描应用&#xff0c;演示了TopJTAG Probe软件的应用&#xff0c;以及边界扫描的基本功能。本文介绍基于Xilinx FPGA的边界扫描应用&#xff0c;两者几乎…

opencv识别一张图片的多个红框,并截取红框的内容

需求 需要获取图片的红框的内容&#xff0c;实体的图片我就不放了 获取红框 先截取获得图片的多个轮廓 import cv2 import numpy as np # 加载图像并转换为灰度图像 image cv2.imread(image6.jpg) gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 应用高斯模糊以减…

保姆级-微信小程序开发教程

一&#xff0c;注册微信小程序 如果你还没有微信公众平台的账号&#xff0c;请先进入微信公众平台首页&#xff0c;点击 “立即注册” 按钮进行注册。注册的账号类型可以是订阅号、服务号、小程序以及企业微信&#xff0c;我们选择 “小程序” 即可。 接着填写账号信息&#x…

文件操作(个人学习笔记黑马学习)

C中对文件操作需要包含头文件<fstream > 文件类型分为两种: 1.文本文件&#xff1a;文件以文本的ASCII码形式存储在计算机中 2.二进制文件&#xff1a;文件以文本的二进制形式存储在计算机中&#xff0c;用户一般不能直接读懂它们 操作文件的三大类: 1.ofstream: 写操作 …

生成多样、真实的评论(2019 IEEE International Conference on Big Data )

论文题目&#xff08;Title&#xff09;&#xff1a;Learning to Generate Diverse and Authentic Reviews via an Encoder-Decoder Model with Transformer and GRU 研究问题&#xff08;Question&#xff09;&#xff1a;评论生成&#xff0c;由上下文评论->生成评论 研…

Java类和对象(七千字详解!!!带你彻底理解类和对象)

目录 一、面向对象的初步认知 1、什么是面向对象 2、面向对象和面向过程 &#xff08;1&#xff09;传统洗衣服的过程 &#xff08;2&#xff09;现代洗衣服过程 ​编辑 二、类的定义和使用 1、类的定义格式 三、类的实例化 1、什么是实例化 2、类和对象说明 四、t…