java EE初阶 —— 线程的安全问题

news2024/11/24 19:15:03

文章目录

  • 1.线程安全
    • 1.1 代码体现线程的不安全
    • 1.2 线程安全问题分析
    • 1.3 产生线程安全问题的原因
    • 1.4 线程安全问题的解决办法
    • 1.5 synchronized 的使用方法

1.线程安全

多线程的抢占式执行,带来的随机性是线程安全问题的罪魁祸首,万恶之源。

如果没有多线程,此时程序代码执行顺序就是固定的。(只有一条路)
代码顺序固定,程序的结果就是固定的。(单线程的情况下,理清这一条路即可)

如果有了多线程,在此时的抢占式执行下,代码执行的顺序会出现很多的变数!!!
代码执行顺序的可能性就从一种情况变为了无数种情况!!!
所以就需要保证这种无数种线程调度顺序的情况下,代码的执行结果都是正确的。

1.1 代码体现线程的不安全

写一个两个线程的代码,计算出count的个数。

package thread;

class Counter {
    public int count = 0;

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

public class ThreadDemo15 {
    public static void main(String[] args) {
        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();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count:" + counter.count);
    }
}

两个线程都调用add方法,输出的结果应该是100000才对。

执行结果:

可以看到实际结果却不是100000。


count++ 操作的**++**本质上要分成三步。

  1. 先把内存中的值读取到 CPU 的寄存器中。(load)
  2. 把 CPU 寄存器的值进行 +1 运算。(add)
  3. 把得到的结果写到内存中。(save)

这三个操作就是 CPU 上的三个指令。


约定一个时间轴,靠上的就是先执行,靠下的就是后执行。

上面就是可能的第一种调度顺序。

由于线程之间是随机调度的,导致此处的调度顺序充满了其他的可能性。



还有其他可能的顺序

第二种:


第三种:


第四种:

因为调度顺序的随机性,是有无数种顺序结果的。

1.2 线程安全问题分析

上面的调度顺序只有前两种是安全的。

下面分析第一种和第二种顺序为什么是安全的。


先按照时间轴的顺序执行 load,把 count 的值读取到 t1



执行addt1里的值变成了1。



执行 savet1的值写回到内存中。


t1的执行完毕,开始执行t2


count 的值读取到 t2 中。



执行addt1里的值变成了2。



执行 savet2的值写回到内存中。
最终 count 等于2,即是两个线程分别调用一次 add 方法计算的结果。

第二种顺序和第一种类似,只是先操作的是t1,这里不在演示。



下面来演示一种 不安全 的顺序。


先按照时间轴的顺序执行 load,将count的值读取到t2中。



执行 load,将count的值读取到t1中。



执行addt2里的值变成了1。



执行 savet2的值写回到内存中。



执行addt1里的值变成了1。



执行 savet1的值写回到内存中。

最终 count 等于1,即是两个线程分别调用一次 add 方法计算的结果。

但是应该的结果是2,所以这是错误的。

由于线程的抢占式执行,导致当前执行到任意一个指令的时候,
线程都可能被调度走,CPU 让别的线程来执行。

这时就会发生上面的安全问题。


上面的代码是有可能输出10w的,但是这个概率非常小。
因为要求两个线程每次的调度顺序都是上述的第一种或者第二种顺序。

也与可能会小于5w,但是也是不确定的。

1.3 产生线程安全问题的原因

1、抢占式执行,随机调度。(根本原因)

2、代码结构:多个线程同时修改同一个变量。(上面代码就是这样的问题)

一个线程,修改一个变量,没事。
多个线程读取同一个变量,没事。
多个线程修改多个不同的变量,没事。

可以调整代码结构代码规避问题,但是不一定都可以使用。


3、原子性:不可拆分的基本单位

如果修改的操作是原子的,那么出现安全问题的概率会比较低。
但是如果修改的是非原子的,出现问题的概率就会非常高了。

count++ 这里可以拆分成 load、add、save 三个操作。
这三个操作是无法再进一步拆分的单个指令。

如果 ++ 操作是原子的,此时线程安全问题就解决了。


4、内存可见性问题(后面说)

如果一个线程读,一个线程该也可能会出现问题。

可能出现此处读的结果不太符合预期。


5、指令重排序(本质上是编译器优化出bug了)

编译器优化:

在逻辑不变的情况下,编译器会调整代码的执行顺序。



这是五个典型的原因,并不是全部的。
如果一个代码踩中了上面的原因,也可能线程是安全的。
如果一个代码没踩中了上面的原因,也可能线程是不安全的。

对于线程是不是安全的,我们还要结合实际、结合需求、具体问题具体分析。
最终抓住原则:多线程运行代码,不出bug,就是安全的!!!

1.4 线程安全问题的解决办法

对于线程安全问题的解决办法,最主要的手段就是从原子性入手
把非原子的操作改为原子的,也就是加锁操作。

把不是原子的,变为原子的,需要 synchronized 关键字来实现。

把上面的代码的Counter类中的add方法加上 synchronized 关键字。

class Counter {
    public int count = 0;

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

加了 synchronized 后,进入方法就会加锁,出了方法就会解锁。

如果两个线程同时加锁,此时一个能加锁成功,另一个只能阻塞等待。(BLOCKED)
一直阻塞到刚才的线程释放锁(解锁)后,当前线程才能加锁成功。



下面来掩饰如何加锁。


在 t1的 load 前面加上一个 lock(加锁),在 t1 的 save 后面加上一个unlock(解锁)

此时的 add 方法多了一个加锁和一个解锁的操作。

t2 的 load前也加一个 lock ,此时 t2 的 lock 到 load 之间处于阻塞状态。
会一直阻塞到 t1 unlock之后,才会让 t2 执行。

lock的阻塞就是把 t2 的 load 推迟到 t1 的 save 之后执行。
这就是在 t1 完成提交数据之后,t2 再来读,也就避免了安全问题。

加锁,说是保证原子性。
其实不是说让这里的三个操作一次完成,也不是使这三步操作过程中不进行调度。
而是让其他也想操作的线程阻塞等待了。

使用阻塞的手段,让 t1 和 t2 按照先是 load,再是 add,最后是 save 这样的顺序执行。


加锁的本质是把并发,变成了串行。


虽然加锁之后,计算的速度会变慢,但是还是会比单线程快。
加锁针对的只是 count++,除此之外的 for循环是可以并发执行的。
只是 count++ 是串行执行的。

在一个任务中可以一部分并发,一部分串行。
即使是这样,也是要比全部是串行快的。



加锁之后,可以看到得到了想要的10w。

1.5 synchronized 的使用方法

  1. 修饰方法
    分为修饰普通方法静态方法

进入方法就加锁,离开方法就解锁。但是这两种方法加锁的对象不同。

修饰普通方法,锁对象就是this
修饰静态方法,所对象就是类对象(Counter.class)

  1. 修饰代码块

修改代码块。显示或者手动的指定锁对象


加锁是要明确是对哪个对象执行加锁的。

如果两个线程对一个对象进行加锁,则会产生阻塞等待。(锁竞争、锁冲突
如果两个线程对不同的对象进行加锁,则不会产生阻塞等待(不会锁冲突、竞争


无论是一个什么样的对象,原则就只有一条:

锁对象相同,就会产生锁竞争(阻塞等待),锁对象不同,就不会产生锁竞争



synchronized 可以写在 public 的左或者右边

直接把 synchronized 修饰到方法上了。
此时相当于 this 加锁。






修饰静态方法和一般方法同理。


synchronized修饰代码块。

进入代码块,就加锁,出了代码块,就解锁。


可以指定任意你想指定的对象,不一定要是 this 。


一个加锁一个不加锁的情况。

class Counter {
    public int count = 0;

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

    public void add2() {
        count++;
    }
}
 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.add2();
     }
 });

第一个线程执行add,第二个线程执行 add2
第一个线程就是加锁了,第二个线程就是没加锁的。

代码的结果说明产生了安全问题。

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

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

相关文章

国内表格软件-FineReport Count函数

1. 概述 1.1 函数作用 计算数组或数据区域中所含项的个数&#xff0c;例如统计「地区数」和「销售员个数」&#xff0c;如下图所示&#xff1a; 也可与其他函数嵌套使用&#xff0c;例如进行「条件计数」&#xff0c;计算除孙林以外的销售员个数&#xff0c;如下图所示&#x…

Lactoferrin-PEG-alginate 乳铁蛋白-聚乙二醇-海藻酸钠

产品名称&#xff1a;乳铁蛋白-聚乙二醇-海藻酸钠 英文名称&#xff1a;Lactoferrin-PEG-alginate 纯度&#xff1a;95% 存储条件&#xff1a;-20C&#xff0c;避光&#xff0c;避湿 外观:固体或粘性液体&#xff0c;取决于分子量 PEG分子量可选&#xff1a;350、550、750、1k、…

VBA驱动SAP GUI自动化:查找页面元素FindAllByName

我们在VBA中嵌入SAP原生的【脚本录制与回放】功能生成的VBS脚本&#xff0c;可以实现很多自动化操作。但只有我们对SAP做了一系列动作&#xff0c;才会得到这些动作的脚本。然而&#xff0c;一旦我们需要用代码提前做一些判断&#xff0c;然后再决定后续的动作时&#xff0c;这…

MBR主引导记录

主引导记录&#xff08;Master Boot Record&#xff0c;缩写&#xff1a;MBR&#xff09;&#xff0c;又叫做主引导扇区&#xff0c;是计算机开机后访问硬盘时所必须要读取的首个扇区&#xff0c;它在硬盘上的三维地址为&#xff08;柱面&#xff0c;磁头&#xff0c;扇区&…

基于ARM的环境参数检测系统设计(Labview+STM32+ZigBee)

目 录 1 绪论 1 1.1 研究背景和意义 1 1.2 研究现状 2 1.3 研究内容 3 2 系统概述和相关原理 4 2.1 系统的功能分析与设计 4 2.2 LabVIEW介绍 5 2.3 ZigBee技术 5 2.3.1 ZigBee技术概述 5 2.3.2 ZigBee网络协议 6 2.3.3 ZigBee网络拓扑结构 7 2.4 GSM技术 8 2.5 本章小结 8 3 …

【研发工具】Centos下搭建轻量级内网FTP服务器

1 前言 vsftpd是一款非常小巧、高性能、稳定性好、安全易用的Linuxt环境下的的FTP服务器软件。 vsftpd 名称是取自 very secure FTP daemon 的缩写&#xff0c;可以在类UNIX类操作系统上运行。 2 部署 通常情况下可以在线安装, # centos / Redhat 操作系统下 $ yum -y ins…

【历史上的今天】12 月 1 日:新浪网成立;钉钉上线;古登堡计划发布

整理 | 王启隆 透过「历史上的今天」&#xff0c;从过去看未来&#xff0c;从现在亦可以改变未来。 今天是 2022 年 12 月 1 日&#xff0c;在 1948 年的今天&#xff0c;中国人民银行成立&#xff0c;并发行了第一套人民币&#xff1b;第一套人民币共印制发行了 12 种面额、6…

推荐系统:架构设计

架构设计概述 架构设计是一个很大的话题&#xff0c;这里只讨论和推荐系统相关的部分。更具体地说&#xff0c;我们主要关注的是算法以及其他相关逻辑在时间和空间上的关系——这样一种逻辑上的架构关系。 在前面的章节中我们讲到了很多种算法&#xff0c;每种算法都是用来解决…

[附源码]JAVA毕业设计个人交友网站(系统+LW)

[附源码]JAVA毕业设计个人交友网站&#xff08;系统LW&#xff09; 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&…

【JavaWeb】Servlet系列 --- 关于一个web站点的欢迎页面

web站点的欢迎页面1. 什么是一个web站点的欢迎页面&#xff1f;2. 怎么访问欢迎页面&#xff1f;静态资源进行变换动态资源3. &#xff08;易错&#xff09;关于WEB-INF目录1. 什么是一个web站点的欢迎页面&#xff1f; 对于一个webapp来说&#xff0c;我们是可以设置它的欢迎…

Java的I/O框架

目录 一、流的概念 二、流的分类 1.按方向 2.按单位&#xff1a; 3.按功能&#xff1a; 三、字节流 1、文件字节流 &#xff08;1&#xff09; FileInputStream&#xff1a; &#xff08;2&#xff09;FileOutputStream&#xff1a; &#xff08;3&#xff09;完成复…

c++基础(自用)

C基础入门 1 C初识 1.1 第一个C程序 编写一个C程序总共分为4个步骤 创建项目创建文件编写代码运行程序 1.1.1 创建项目 ​ Visual Studio是我们用来编写C程序的主要工具&#xff0c;我们先将它打开 1.1.2 创建文件 右键源文件&#xff0c;选择添加->新建项 给C文件起…

Windows8系统下DOSBox编译、链接、执行汇编语言步骤

下载安装好DOSBox后&#xff0c;同时下载debug、link、masm程序。 &#xff08;1&#xff09;建立工作目录&#xff0c;编写汇编语言源文件&#xff0c;并将debug、link、masm程序放在同一目录下。&#xff08;下图中ass.asm是汇编语言源文件&#xff09; &#xff08;2&#x…

2022年第四届大数据与计算国际研讨会(WBDC 2022)

2022年第四届大数据与计算国际研讨会&#xff08;WBDC 2022&#xff09; 重要信息 会议网址&#xff1a;www.iwbdc.org 会议时间&#xff1a;2022年12月16-18日 召开地点&#xff1a;中国北京 截稿时间&#xff1a;2022年10月31日 录用通知&#xff1a;投稿后2周内 收录检…

MPI 快速入门

浅学 MPI。 MPI 分布式内存多处理器&#xff1a; 处理器 辅助组件 > 节点一堆节点 > 高性能计算系统 节点 > 进程 节点之间&#xff1a;消息传递 MPI&#xff1a;消息传递接口 安装 还是用 Docker 方便。 宿主机&#xff1a; sudo docker run -idt --name op…

移动WEB开发之流式布局--移动WEB开发之flex布局--携程网首页案例制作

案例&#xff1a;携程网移动端首页 访问地址&#xff1a;携程旅行-酒店预订,机票预订查询,旅游度假,商旅管理-携程无线官网 (ctrip.com) 1. 技术选型 方案&#xff1a;我们采取单独制作移动页面方案 技术&#xff1a;布局采取flex布局 2. 搭建相关文件夹结构 3. 设置视口标…

三、【react-redux】数据共享

文章目录1、优化项目结构2、添加一个新容器组件2.1、新项目结构2.2、CODE2.2.1、reduc/constant.js2.2.2、redux/actions/person.js2.2.3、redux/reducers/person.js2.2.4、redux/store.js2.2.5、Count.jsx2.2.6、Person.jsx2.3、Result3、总结本示例修改自 上一章 求和Demo 1、…

SVN版本控制软件

尚硅谷SVN版本控制软件教程&#xff08;一套掌握svn操作&#xff09; 学习网址&#xff1a;https://www.bilibili.com/video/BV1mW411M7yR/?spm_id_from333.999.0.0&vd_source461545ff50a35eaeaa8218ecdc5f7152 学习时长&#xff1a;1小时46分钟 未学习 5.启动服务器 6.…

维视智造明星产品推荐(一) 环外侧工业镜头

维视智造明星产品推荐&#xff08;一&#xff09;环外侧工业镜头 市场洞察 产品外观质量检测及标签检测&#xff0c;是工业制造中常见的两个质检场景。根据产品特点及产线环境&#xff0c;往往可以做多种检测方案的选择。在圆柱状产品如药瓶、瓶盖、齿轮、螺母等的生产检测中&a…

11.30排序

目录 一.排序 1.概念 1.1排序 1.2稳定性 2.七大基于比较的排序 二.插入排序 3.1 直接插入排序-原理 2.折半插入排序 3.分析 二.每日一题订正 1.选择题 2.不要二 三.希尔排序 1 原理 2.代码实现 3.分析 四.选择排序 1.原理 2.代码 3.优化版 4.分析 五.测量…