线程安全的问题以及解决方案

线程安全

线程安全的定义

线程安全:某个代码无论是在单线程上运行还是在多线程上运行,都不会产生bug.

线程不安全:单线程上运行正常,多线程上运行会产生bug.

观察线程不安全

看看下面的代码:

public class ThreadTest1 {
    public static int count = 0;

    public static void main(String[] args)throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                count++;
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                count++;
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();

        System.out.println(count);
    }
}

按照常理来讲,运行的结果应该是20000,但让我们来看看实际的运行结果: 

显然结果与我们预期的不一致,但为什么会出现这种问题呢?

让我们来看一下线程不安全的原因:

线程不安全的原因

重点:线程调度是随机的 

1.根本原因:操作系统上线程是"抢占式执行",而且是"随机调度"的,执行顺序会有很多变数.(罪魁祸首)

2.代码结构:多个线程同时修改一个变量(1. 一个线程修改一个变量(没事) 2.多个线程同时读取一个变量(没事) 3.多个线程同时修改不同的变量(没事))

3.直接原因:上述线程的修改操作本身不是"原子的",比如count++这条语句,它本身包含多个cpu指令(这个例子后面会详细讲).执行了一半可能会调度走.

4.内存可见性问题(例子里的代码还没有),后面的文章会讲.

5.指令重排序问题

分析例子代码中的问题

这个问题就主要出现在count++这条语句中.它本身包含这些cpu指令:LOAD,ADD,SAVE

让我们回顾一下这几条指令的含义:

(1)load:从内存中读取数据到cpu的寄存器

(2)add:把寄存器中的值+1,

(3)save:把寄存器的值写回到内存中.

因此count++这条语句的执行的流程如下:

这是一个count++的执行流程,但是在多进程程序中,这三条指令一定会连贯执行吗(规范的按照一个load->add->save执行)? ,留着这个问题,来看看后面的内容:

修改共享数据

在例子中,显然是符合多个线程修改同一个变量的.

上面线程不安全的代码中,涉及到多个线程对count变量进行的修改.

此时这个count是一个多线程都能访问到的共享数据,因此t1和t2都可以对count进行修改.

原子性

什么是原子性

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人.如果没有任何机制保证,A进入房间之后,还没有出来;B是不是也可以进入房间,打断A在房间中的隐私.这个就是不具备原子性的.

不保证原子性会给多线程带来什么问题

如果一个线程正在对一个变量操作,中途其它的线程插进来了,如果这个操作被打断了,结果可能就是错误的.

这点也和线程的抢占式调度密切相关,如果线程不是"抢占"的,就算没有原子性,也问题不大.

综合以上,我们可以得到引起问题的原因:共享数据的修改以及数据并非原子的.

通过下面这个图就可以看出来:

等等还有很多种执行顺序(无数种).

比如图二:由于t2的load抢占在t1的add前执行,因此导入时count值都一样,那么执行的结果最后就是+1,而不是理想中的各自线程都给count+1,最后执行完两个就是+2了.那么有没有一种情况执行结果是正常的,当然有:

类似这种每个线程执行时,三条指令都是在一块的,这种运行是正确的,那么有没有一种方法能按照这样运行呢?有的.

只要将count++操作上锁,使得这三条一起指令执行完之后,才会执行下一个操作.

 有时也把这个现象叫做同步互斥,表示操作是互相排斥的.

解决上面的问题
public class ThreadTest {
    public static final Object locker = new Object();
    public static int count = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                //进入大括号会上锁
                synchronized (locker) {
                    count++;
                }//出大括号会解锁
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

这里用到的机制(synchronized)后面的文章会解释. 

可见性

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

Java内存模型(JMM) :Java虚拟机规范中定义了java内存模型.

目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.

线程之间的共享变量存在主内存(可以看作为上面的内存)

每一个线程都有自己的工作内存(并不是真正的内存,可以看作为上面的cpu寄存器(也有可能是cpu缓存,不过都差不多))

当线程要读取一个共享变量的时候,会先把变量从主内存拷贝到工作内存,再从工作内存种读取数据.

当线程要修改一个共享变量的时候,也会先修改工作内存中的副本,再同步回主内存.

(1)初始情况下,两个线程的工作内存一致

(2)一旦线程1修改了a的值,此时主内存并不一定可以及时同步过来(是在寄存器中改动的,因为寄存器比较快) 

此时引入了一个问题:

为什么要在主内存和工作内存种麻烦的拷来拷去?

因为CPU访问自身寄存器的速度以及高速缓存的速度,远远超过访问内存的速度(快了几千至上万倍) 


比如某个代码种要连续10次读取某个变量的值,如果10次都从内存中度,速度是很慢的.但如果只是第一次从内存中读,读到的结果缓存到CPU某个寄存器中,那么后面9次就不需要从内存中读了,效率就大大提高了.

那么问题又来了,既然寄存器速度这么快,还要内存干嘛?

贵!

后面我们将用更详细的方法解决线程安全问题,敬请期待.

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Windows驱动中数字签名认证(使用 ci.dll)

1.背景 对于常规应用程序来说&#xff0c;在应用层可以使用 WinVerifyTrust, 在驱动层使用常规的 API无法使用&#xff0c;自己分析数据又太麻烦。 但在内核中 ci.dll 包装了数据签名验证相关的功能&#xff0c;我们可以使用该 dll 来实现我们的数字签名验证。 详细的分析见《内…

《异常检测——从经典算法到深度学习》24 用于单变量时间序列异常检测的端到端基准套件

《异常检测——从经典算法到深度学习》 0 概论1 基于隔离森林的异常检测算法 2 基于LOF的异常检测算法3 基于One-Class SVM的异常检测算法4 基于高斯概率密度异常检测算法5 Opprentice——异常检测经典算法最终篇6 基于重构概率的 VAE 异常检测7 基于条件VAE异常检测8 Donut: …

面试题:千万量级数据中查询 10W 量级的数据有什么方案?

文章目录 前言初版设计方案整体方案设计为&#xff1a;技术方案如下&#xff1a;CK 分页查询使用 ES Scroll Scan 优化深翻页耗时数据 ESHbase 组合查询方案ES 查询的两个阶段组合使用 Hbase RediSearchRedisJSON 优化方案RediSearch 性能数据RedisJSON 性能数据 总结 前言 在…

C++跨目录include问题

不同文件夹下使用预处理器指示符#include 使用举例 假设我们有如下一个工程&#xff0c;其中包含了几个源代码和头文件&#xff0c;其中main.cpp是主源代码文件&#xff0c;里面含有main函数&#xff1a; 在foldder main中包含&#xff1a;func4.hpp&#xff0c;func4.cpp&am…

MIT线性代数笔记-第21讲-特征值,特征向量

目录 21.特征值&#xff0c;特征向量打赏 21.特征值&#xff0c;特征向量 对于一个方阵 A A A&#xff0c;若 A x ⃗ λ x ⃗ A \vec{x} \lambda \vec{x} Ax λx &#xff0c;即 A x ⃗ A \vec{x} Ax 平行于 x ⃗ \vec{x} x &#xff0c;那么 λ \lambda λ是 A A A的特征值…

100W用户、8000W流量在线贺卡应用架构如何优化?

文章目录 &#x1f50a;博主介绍&#x1f964;本文内容&#x1f4e2;文章总结&#x1f4e5;博主目标 &#x1f50a;博主介绍 &#x1f31f;我是廖志伟&#xff0c;一名Java开发工程师、Java领域优质创作者、CSDN博客专家、51CTO专家博主、阿里云专家博主、清华大学出版社签约作…

FL Studio2024水果编曲软件21.2.0中文版本下载更新

FL Studio2024是功能强大的音乐制作解决方案&#xff0c;使用旨在为用户提供一个友好完整的音乐创建环境&#xff0c;让您能够轻松创建、管理、编辑、混合具有专业品质的音乐&#xff0c;一切的一切都集中在一个软件中&#xff0c;只要您想&#xff0c;只要您需要&#xff0c;它…

Leetcode—409.最长回文串【简单】

2023每日刷题&#xff08;四十八&#xff09; Leetcode—409.最长回文串 强烈吐槽&#xff01;&#xff01;&#xff01; 非常不理解&#xff0c;同样的代码&#xff0c;为什么C跑不了C就跑得了&#xff0c;力扣编译器是对C语言有歧视吗&#xff1f;&#xff1f;&#xff1f;…

【C++练级之路】【Lv.1】C++,启动!(命名空间,缺省参数,函数重载,引用,内联函数,auto,范围for,nullptr)

目录 引言入门须知一、命名空间1.1 作用域限定符1.2 命名空间的意义1.3 命名空间的定义1.4 命名空间的使用 二、C输入&输出2.1 cout输出2.2 cin输入2.3 std命名空间的使用惯例 三、缺省参数3.1 缺省参数概念3.2 缺省参数分类 四、函数重载4.1 函数重载概念4.2 函数重载分类…

6-63.圆类的定义与使用(拷贝构造函数)

本题要求完成一个圆类的定义&#xff0c;设计适当的函数&#xff1a;包括构造函数、拷贝构造函数以及析构函数&#xff0c;从而可以通过测试程序输出样例 在这里给出一组输入。例如&#xff1a; 5 输出样例&#xff1a; 在这里给出相应的输出。例如&#xff1a; Constructo…

【Unity动画】状态机添加参数控制动画切换(Animator Controller)

Unity - 手册&#xff1a;动画参数 在Unity中&#xff0c;动画状态的切换是通过Animator Controller中的过渡&#xff08;Transition&#xff09;来实现的。过渡是状态之间的连接&#xff0c;控制过渡一般都是靠调用代码参数 我们来实现一个案例&#xff1a; 创建动画状态机&a…

Pycharm修改文件默认打开方式 + CSV Editor插件使用

1、File —> Settings —> Editor —> File Types 然后将*csv添加到最上面 在plugins中下载插件&#xff0c;CSV Editor 备注&#xff1a;不在上一步的“File Types”中将*.csv设置为CSV格式&#xff0c;插件是不起作用的 就可以使用了

GitHub Actions 之自动化发布 Maven 项目

开发开源数据中台项目 datacap 时&#xff0c;之前发布版本都是通过在本地编译并部署到 maven 中央仓库中&#xff0c;这样就导致是非自动化工程&#xff0c;于是通过搜索发现 samuelmeuli/action-maven-publish 这个 github 自动化工具可以帮助我们来做这些事情&#xff0c;本…

显示隐藏文件

win 查看-勾选隐藏文件 mac shiftcommond.

食物相关的深度学习数据集合集—食物、饮料、肉类、餐具等数据集

最近收集了一大波与食物酒水相关的数据集&#xff0c;包含食物、饮料、肉类、餐具等不同等类型的数据集&#xff0c;废话不多说&#xff0c;给大家逐一介绍&#xff01;&#xff01; 1、自制啤酒配方数据库 超过20万自制啤酒配方数据库&#xff0c;数据集包含不同精酿啤酒的名…

麒麟系统自定义服务-开机自启-配置方案

方法一 &#xff1a;使用systemd的service文件自定义开机启动服务 一、kylin 添加自定义脚本服务说明 在kylin 下&#xff0c;使用管理unit的方式来控制开机自启动服务和添加自定义脚本服务。在/usr/lib/systemd/system目录下包含了各种unit文件&#xff0c;有service后缀的服…

C语言每日一题(44)删除排序链表中的重复元素 II

力扣 82 删除排序链表中的重复元素 II 题目描述 给定一个已排序的链表的头 head &#xff0c; 删除原始链表中所有重复数字的节点&#xff0c;只留下不同的数字 。返回 已排序的链表 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,3,4,4,5] 输出&#xff1a;[1,2,5]示…

【msg_msg】corCTF2021-msgmsg 套题

前言 该套题共两题&#xff0c;一道简单模式 fire_of_salvation&#xff0c;一道困难模式 wall_of_perdition&#xff0c;都是关于 msg_msg 的利用的。这题跟之前的 TPCTF2023 core 的很像&#xff08;应该是 TPCTF2023 core 跟他很像&#xff0c;bushi&#xff09;。 其中 f…

打造个性化github主页 一

文章目录 概述创建仓库静态美化GitHub 统计信息卡仓库 GitHub 额外图钉仓库 热门语言卡仓库 GitHub 资料奖杯仓库 GitHub 活动统计图仓库 打字特效添加中文网站统计仓库 总结 概述 github作为全球最大的代码托管平台&#xff0c;作为程序员都多多少少&#xff0c;都使用过他。…

如何在没有备份的情况下从 Android 手机恢复已删除的数据

电话数据对我们至关重要。我们可以更换我们使用的设备&#xff0c;但不能更换我们的数据。我们以前一直使用CD、USB 和硬盘来保存数据。随着技术的出现&#xff0c;我们遇到了云存储。我们可以随时随地、任意次数地访问存储。所有操作系统都有数据云&#xff0c;可以用来保存图…