多线程带来的的风险-线程安全、锁的问题

线程安全的概念

想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:

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

观察线程不安全

class Counter {
    public int count = 0;

    public void add() {
        count++;
    }
}

public class ThreadDemo13 {
    public static void main(String[] args) {
        Counter counter = new Counter();

        // 搞两个线程, 两个线程分别针对 counter 来 调用 5w 次的 add 方法
        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();

        // 等待两个线程结束
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 打印最终的 count 值
        System.out.println("count = " + counter.count);
    }
}

        预想的情况是两个线程各自增5w次,运行结束 count 的值应该为10w,但结果每次不一样,因此我们就说出现了 bug。

线程不安全的原因

1.【根本原因】抢占式执行,随即调度

2.代码结构:多个线程同时修改同一个变量。这种可以通过调整代码结构来规避这个问题,但是这种调整不是一种普遍性高的方案(因为代码结构是源于 需求 的,改了之后可能达不到需求或者性价比太低)

3.原子性:如果改写操作是原子性的,那还没啥事;但是是非原子的,出现线程安全问题的概率就非常高了。(原子:不可拆分的基本单位)

像上面的 count++,可以拆分成 load,add,sava 这三个操作。所以 ++ 操作并不是原子性的,因此我们想要解决线程安全问题,就需要把这个操作弄成原子性。也就是通过加锁的操作。这是解决线程安全问题最主要的手段。

synchronized public void add() {
    count++;
}

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

        如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。这点也和线程的抢占式调度密切相关. 如果线程不是 "抢占" 的, 就算没有原子性, 也问题不大.

4.内存可见性问题

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

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

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

线程之间的共享变量存在 主内存 (Main Memory).

每一个线程都有自己的 "工作内存" (Working Memory) .

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

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

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

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

2) 一旦线程1 修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的工作内存的 a 的值也不一定能及时同步.

这个时候代码中就容易出现问题.

此时引入了两个问题:

1) 为啥整这么多内存?

实际并没有这么多 "内存". 这只是 Java 规范中的一个术语, 是属于 "抽象" 的叫法.

所谓的 "主内存" 才是真正硬件角度的 "内存". 而所谓的 "工作内存", 则是指 CPU 的寄存器和高速缓存.

2) 为啥要这么麻烦的拷来拷去?

因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级, 也就是几千倍, 上万倍).

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

那么接下来问题又来了, 既然访问寄存器速度这么快, 还要内存干啥?? 因为寄存器贵!

值的一提的是, 快和慢都是相对的. CPU 访问寄存器速度远远快于内存, 但是内存的访问速度又远远快于硬盘.对应的, CPU 的价格最贵, 内存次之, 硬盘最便宜.

5.指令重排序

有一段代码是这样的:

1. 去楼下取下外卖

2. 回房间写 10 分钟作业

3. 去楼下取下快递

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

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

重排序是一个比较复杂的话题, 涉及到 CPU 以及编译器的一些底层工作原理, 此处不做过多讨论

synchronized 关键字 — 监视器锁monitor lock

synchronized 的特性

1) 互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.

进入 synchronized 修饰的代码块, 相当于 加锁

退出 synchronized 修饰的代码块, 相当于 解锁

举个例子,加锁就像是上厕所:

如果当前是 "无人" 状态, 那么就可以使用, 使用时需要设为 "有人" 状态.

如果当前是 "有人" 状态, 那么其他人无法使用, 只能排队,理解 "阻塞等待".

注意:

上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这也就是操作系统线程调度的一部分工作.

假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争。

关于锁对象的规则:

        1.如果两个线程针对同一个对象加锁,此时就会出现 锁竞争 / 锁冲突 的问题,其中的一个线程获取到了锁(先到先得),另一个线程只能阻塞等待,等到那个线程解锁了,这个线程才能取锁成功。

       2.如果两个线程针对同一个线程,一个加锁一个不加锁,这个时候就没有 锁竞争 / 锁冲突 的问题,但是却会发生线程安全问题。

       3.如果两个线程针对两个对象加锁,则不会出现 锁竞争 / 锁冲突 的问题,各自获取到各自的锁,不会有阻塞等待。

        

2) 可重入

        一个线程针对同一个对象连续进行两次加锁,是否会出现问题?如果没问题,则是可重入锁;如果有问题,就叫做不可重入锁。

代码示例

    synchronized public void add() {  // 锁对象是 this
        synchronized (this) {
            count++;
        }
    }

        只要有线程调用 add ,进入 add 方法的时候就会进行加锁,紧接着又遇到了代码块。此时站在 this(锁对象)的视角,它认为自己已经被别的线程占用了(上面的代码是个特殊情况:别的线程其实就是这个线程),这里的第二次加锁是否要阻塞等待?如果需要阻塞等待,那么就是不可重入的,这个情况就会导致线程“僵住”,也就是死锁了。

        因为Java 中的 synchronized 是 可重入锁, 因此没有上面的问题。会在锁对象里记录一下,如果当前加锁线程和持有锁的线程是同一个,就会直接通过,不会阻塞。

死锁的四个必要条件:

1.互斥使用:线程 1 拿到了锁,线程 2 就得等着(锁的基本特性)

2.不可抢占:线程 1 拿到锁之后,必须是线程 1 主动释放。不能是线程 2 把锁强行获取到

3.请求和保持:线程 1 拿到 锁A 之后,再尝试获取 锁B ,A 这把锁还是保持的(不会因为获取 B 就把 A 释放了)

4.循环等待:线程 1 尝试获取 A 和 B ,线程 2 尝试获取 B 和 A 。                                                                         线程 1 在获取 B 的时候等到线程 2 释放 B ;同时线程 2 在获取 A 的时候等待线程 1 释放 A 

        前三个条件都是锁的基本特性,改不了;第四点是唯一一个和代码相关的,也是我们可以控制的,因此解决死锁的办法就是给锁编号,按固定的顺序(从小到大)来加锁。

死锁几种常见的情况:

        1.一个线程,一把锁,连续加锁两次,如果锁是不可重入锁,就会出现死锁。

        2.两个线程两把锁,t1 和 t2 各自先针对 锁A 和 锁B 加锁,再尝试获取对方的锁。

public class ThreadDemo14 {
    public static void main(String[] args) {
        // 假设 jiangyou 是 1 号, cu 是 2 号, 约定先拿小的, 后拿大的.
        Object jiangyou = new Object();
        Object cu = new Object();

        Thread tanglaoshi = new Thread(() -> {
            synchronized (jiangyou) {

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (cu) {
                    System.out.println("汤老湿把酱油和醋都拿到了");
                }
            }
        });
        Thread shiniang = new Thread(() -> {
            synchronized (cu) {

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (jiangyou) {
                    System.out.println("师娘把酱油和醋都拿到了");
                }
            }
        });
        tanglaoshi.start();
        shiniang.start();
    }
}

这个代码的逻辑是:老师先拿了酱油,师娘先拿了醋,这时候俩人都想混合吃,老师说你先把醋给我,等我倒完了再给你,师娘不愿意,结果就僵持住了,出现了死锁。

        3.多个线程多把锁(相当于 2 的更进一步)。锁更多,线程更多,情况也就更复杂了。因此根据上述死锁条件的第四点作为突破口就能解决了。

 Thread shiniang = new Thread(() -> {
            synchronized (jiangyou) {

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (cu) {
                    System.out.println("师娘把酱油和醋都拿到了");
                }
            }
        });

synchronized 使用示例

1) 直接修饰普通方法

synchronized public void add() {
    count++;
}

2) 修饰静态方法

synchronized public static void add() {
     count++;
}

3) 修饰代码块: 明确指定锁哪个对象.

锁当前对象

public void add() {
// 进入代码块就加锁
    synchronized (this) {  // 这里可以指定任意想指定的对象,不一定非得是 this
        count++;
    }
// 出了代码块就解锁
}

Java 标准库中的线程安全类

        Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 因此需要我们自己手动加锁

ArrayList、LinkedList、HashMap、TreeMap、HashSet、TreeSet、StringBuilder

但是还有一些是相对线程安全的,已经内置了 synchronized 加锁。使用了一些锁机制来控制.

Vector (不推荐使用)、HashTable (不推荐使用)、ConcurrentHashMap、StringBuffer

还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的

String

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

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

相关文章

C++初阶 | [十一] priority_queue 优先级队列

摘要&#xff1a;priority_queue 优先级队列的使用和模拟实现&#xff0c;仿函数 前言——优先级队列介绍&#xff1a; 优先队列是一种容器适配器&#xff0c;根据严格的弱排序标准&#xff0c;它的第一个元素总是它所包含的元素中最大的。此上下文类似于堆&#xff0c;在堆中…

OpenHarmony实战:用IPOP调试 OpenHarmony 内核

前言 我使用的是 IPOP V4.1&#xff0c;基于 OpenHarmony 开源系统和 RK3568 开发板&#xff0c;在 PC 上运行此软件&#xff0c;查看运行、错误日志来调试内核。作为网络、嵌入式式内核调试的必备工具&#xff0c;建议同学珍藏。IPOP 运行在 PC 上&#xff0c;操作系统是 Win…

真快乐APP抢购源码实现

支持多个平台的自动 滑动验证码、选字验证码。缺点就是需要自己找一个验证码识别服务器,可以自己用python写一个,或者使用超级鹰(本篇教程就是使用它) 下面是实现源码 "ui"; Date.prototype.Format = function (fmt) {var o = {"M+": this.getMonth() …

学习transformer模型-broadcast广播的简明介绍

broadcast的定义和目的&#xff1a; 广播发生在将较小的张量“拉伸”以具有与较大张量兼容的形状&#xff0c;以便执行操作时。 广播是一种有效执行张量操作而不创建重复数据的方式。 广播的处理过程&#xff1a; 1&#xff0c; 确定最右边的维度是否兼容 每…

前端性能优化-Table渲染速度优化

教务系统-排课页面性能优化总结 一、前言 在公司教务系统中,排课页面慢的令人发指,在某些情况由于数据量大导致页面主进程卡死,遂组织进行一次排查优化,现记录一下 二、效果对比 以下数据均为UAT环境 Performence对比 更改前: 主进程渲染时间为 8s 教务系统-排课页面性…

Intel FPGA (6):dac tlv5618a

Intel FPGA (6)&#xff1a;dac tlv5618a 前提摘要 个人说明&#xff1a; 限于时间紧迫以及作者水平有限&#xff0c;本文错误、疏漏之处恐不在少数&#xff0c;恳请读者批评指正。意见请留言或者发送邮件至&#xff1a;“Email:noahpanzzzgmail.com”。本博客的工程文件均存放…

探索DeFi元宇宙:NFT、Web3和DAPP的数藏Swap合约应用开发

随着区块链技术的发展和普及&#xff0c;DeFi&#xff08;去中心化金融&#xff09;和NFT&#xff08;非同质化代币&#xff09;等概念在数字经济中扮演着越来越重要的角色。而元宇宙、Web3、DAPP等概念也逐渐成为人们关注的焦点。在这个背景下&#xff0c;将这些概念融合在一起…

Flutter仿Boss-2.启动页、引导页

简述 在移动应用开发中&#xff0c;启动页和引导页是用户初次接触应用时的重要组成部分&#xff0c;能够提升用户体验和导航用户了解应用功能。本文将介绍如何使用Flutter实现启动页和引导页&#xff0c;并展示相关代码实现。 启动页 启动页是应用的第一个页面&#xff0c;首…

Excel 粘贴回筛选后的单元格不能完全粘老是少数据 ,有些单元格还是空的

环境&#xff1a; excel2021 Win10专业版 问题描述&#xff1a; excel 粘贴回筛选后的单元格不能完全粘老是少数据 有些单元格还是空的 复制选择筛选后A1-A10单元格 &#xff0c;定位条件&#xff09;&#xff08;仅可见单元格&#xff09;来访问&#xff0c;或者你可以使用…

Python接口自动化测试-篇1(postman+requests+pytest+allure)

Python接口自动化测试是一种使用Python编程语言来编写脚本以自动执行针对应用程序接口&#xff08;APIs&#xff09;的测试过程。这种测试方法专注于检查系统的不同组件或服务之间的交互&#xff0c;确保它们按照预期规范进行通信&#xff0c;而不涉及用户界面&#xff08;UI&a…

项目部署到线上byte[]转换中文乱码,本地是好的

项目部署到线上byte[]转换中文乱码&#xff0c;本地是好的 byte[]转换成中文乱码&#xff0c;在idea上面调试没有乱码&#xff0c;部署到线上就乱码&#xff0c;原因是idea启动项目是utf-8, 然后线上是windows环境不知道啥 vo.setJsonObject(JSONUtil.parseObj(Convert.toStr(…

uni app 扫雷

闲来无聊。做个扫雷玩玩吧&#xff0c;点击打开&#xff0c;长按标记&#xff0c;标记的点击两次或长按取消标记。所有打开结束 <template><view class"page_main"><view class"add_button" style"width: 100vw; margin-bottom: 20r…

瑞_23种设计模式_迭代器模式

文章目录 1 迭代器模式&#xff08;Iterator Pattern&#xff09;★★★1.1 介绍1.2 概述1.3 迭代器模式的结构1.4 中介者模式的优缺点1.5 中介者模式的使用场景 2 案例一2.1 需求2.2 代码实现 3 案例二3.1 需求3.2 代码实现 4 JDK源码解析 &#x1f64a; 前言&#xff1a;本文…

TypeScript语法快速上手

TypeScript语法 对比ts编译器类型注解新增类型数组自定义类型注解函数类型对象类型元组类型类型推断枚举类型 对比 最大区别&#xff1a;ts能在编译时就能发现类型错误的问题&#xff0c;而js只有在系统运行时再通过异常中断来发现 ts的底层仍是js&#xff0c;但ts能够有效减少…

思腾推出支持大规模深度学习训练的高性能AI服务器

近日人工智能研究公司OpenAI公布了其大型语言模型的最新版本——GPT-4&#xff0c;可10秒钟做出一个网站&#xff0c;60秒做出一个游戏&#xff0c;参加了多种基准考试测试&#xff0c;它的得分高于88%的应试者&#xff1b;随后百度CEO李彦宏宣布正式推出大语言模型“文心一言”…

element-ui message 组件源码分享

今日简单分享 message 组件的源码&#xff0c;主要从以下四个方面来分享&#xff1a; 1、message 组件的页面结构 2、message 组件的 options 配置 3、mesage 组件的方法 4、个人总结 一、message 组件的页面结构 二、message 组件的 options 配置 前置说明&#xff1a;m…

Meta Pixel:助你实现高效地Facebook广告追踪

Meta Pixel 像素代码是用來衡量Facebook广告效果的一个官方数据工具&#xff0c;只要商家有在Facebook上投放广告就需要串联Meta Pixel 像素代码来查看相关数据。 它本质上是一段 JavaScript 代码&#xff0c;安装后可以让用户在自己网站上查看到访客活动。它的工作原理是加载…

FPGA高端图像处理开发板-->鲲叔4EV:12G-SDI、4K HDMI2.0、MIPI等接口谁敢与我争锋?

目录 前言鲲叔4EV----高端FPGA图像处理开发板核心板描述底板描述配套例程源码描述配套服务描述开发板测试视频演示开发板获取 前言 在CSDN写博客传播FPGA开发经验已经一年多了&#xff0c;帮助了不少人&#xff0c;也得罪了不少人&#xff0c;有的人用我的代码赢得了某些比赛、…

基于FPGA的HDMI视频接口设计

HDMI介绍 HDMI(High-DefinitionMultimedia Interface)又被称为高清晰度多媒体接口,是首个支持在单线缆上传输,不经过压缩的全数字高清晰度、多声道音频和智能格式与控制命令数据的数字接口。HDMI接口由Silicon Image美国晶像公司倡导,联合索尼、日立、松下、飞利浦、汤姆逊、东…

使用 Django 构建简单 Web 应用

当我们在使用Django构建Web应用时&#xff0c;通常将会涉及到多个步骤&#xff0c;从创建项目到编写视图、模板、模型&#xff0c;再到配置URL路由和静态文件&#xff0c;最后部署到服务器上。所以说如果有一个环节出了问题&#xff0c;都是非常棘手的&#xff0c;下面就是我们…