锋盈数科-知识库 Logo
首页
软件开发
计算机基础
Hello Halo
新手必读
关于本知识库
登录 →
锋盈数科-知识库 Logo
首页 软件开发 计算机基础 Hello Halo 新手必读 关于本知识库
登录
  1. 首页
  2. 软件开发
  3. 多线程编程带来的不安全问题

多线程编程带来的不安全问题

0
  • 软件开发
  • 发布于 2024-09-19
  • 0 次阅读
黄健
黄健

作者:敲代码の流川枫

博客主页:流川枫的博客

专栏:和我一起学java

语录:Stay hungry stay foolish

给大家推荐一款好用的神器
Apifox = Postman + Swagger + Mock + JMeter。集接口文档工具、接口Mock工具、接口自动化测试工具、接口调试工具于一体,提升 10 倍研发效率戳我来体验\~

目录

1.观察线程不安全问题

2.出现线程不安全问题原因

2.1 根本原因

2.2 代码结构

2.3 原子性

2.4 内存可见性问题

2.5指令重排序

3.通过原子性解决线程安全问题

4.synchronized的使用方法

4.1 修饰方法

4.2 修饰代码块


1.观察线程不安全问题

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

如果没有多线程,代码的执行顺序就是固定的,代码执行顺序固定,那么程序的结果也就是固定的,有了多线程,在抢占式执行,随即调度的机制下,代码的执行顺序就会发生改变,产生更多的变数!

代码执行顺序的可能性从一种情况变成无数种执行顺序,所以要保证无数种线程调度顺序的情况下,代码执行结果是正确的,如果不正确,线程就是不安全的!!

下面通过代码感受线程安全问题

我们定义两个线程,分别对count进行自增50000次,我们的预期结果是count为100000,在线程安全的前提下是这样的,如果线程不安全,结果肯定有差异,那如何让线程安全呢?

class Counter{
    public static int count = 0;
    public  void add(){
        count++;
    }

}
public class ThreadDemo14 {

    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) {
            throw new RuntimeException(e);
        }
        System.out.println("counter = "+counter.count);
    }
}

多次运行后的结果

我们预期的结果是十万,这里的结果不是并且每次结果都不一样,因此这是一个多线程带来的bug!!!

我们分析一下原因

自增一次操作分为三步,先把内存中的值读取到cpu的寄存器中(load),把cpu中的数值+1运算(add),最后把结果写到内存中(save)

由于线程是抢占式执行,两个线程中的这三个步骤中执行到任意一个指令时,线程都可能被被调度走,cpu让别的线程来执行

其中,这两个情况是没有问题的,线程安全

第一种情况

t1线程执行三个步骤,对count++后count为1,然后t2对count操作,count++后写入内存中,自增两次,count结果为2,正确.另一种情况和这种情况相反,t2先让count ++后写入内存,此时t1加载出的count是1,自增后count为2,写入内存

来看这种线程不安全的情况

t1先加载,并且自增,count等于1,但是此时没有写入内存,t2线程就开始加载,count为0,自增后为1,然后写入内存,count为1,然后t1线程写入内存,count还是1!!!就在这里出现了问题

这种情况也不安全,t1先load,此时count=0,然后t2load, 此时count=0.然后自增,count=1,写入内存中,然后t1自增,count=1,写入内存中,后一次的自增覆盖了前一次的自增,count还是1!经历了两次自增,count结果还是1,这就出bug了!

类似于之前提到事务"读未提交 read uncommitted"是相同的,相当于t1读到的是t2还没来得及提交的数据,并发事务和多线程都属于并发编程问题

这就是count结果不是十万的原因,但是也有可能结果恰好是正确的,如果线程每次调度的顺序都是上面提到的正确的顺序,结果就是正确的,但是可能性很小!

结果不是10W,那么结果一定都会大于5w吗,也不一定,如果全都出现了两个线程都自增一次,count+1这种情况,结果就不大于5w,或者,t1自增1次,t2自增了2次,count最终还是+1这种情况,就小于5w

这就属于,t2中的count自增无论多少次,还是会被t1最后给覆盖了,(t1load时count为0)count还是1

2.出现线程不安全问题原因

2.1 根本原因

抢占式执行,随即调度

2.2 代码结构

多个线程同时修改同一个变量(一个线程修改一个变量,安全.多个线程读取同一个变量,安全,像String对象,不可变对象,天然石线程安全的,无法修改,只能读取.多个线程修改多个不同的变量,安全)

因此可以通过调整代码结构来规避这个问题,这种调整不一定都能使用,不是一个普适性的方案

2.3 原子性

如果修改操作是原子的,那就线程安全,但是像上述案例中,非原子的,那就有很大概率出现线程安全问题!

那么如何让操作变成原子的呢?引入了重要的概念—-加锁

2.4 内存可见性问题

如果一个线程读,一个线程改,那么读到的结果可能不符合预期

2.5指令重排序

本质是编译器的优化出问题了 ,把代码调整了,保持逻辑不变的情况下,调整代码的执行顺序,可能会出问题

这几个问题是典型的线程不安全问题的原因,但是线程是否安全远不止这些原因,多线程运行代码 ,不出bug就是安全的!!

3.通过原子性解决线程安全问题

通过"加锁操作"把非原子的操作转化为原子的操作

我们对第一个案例进行改动

对方法加了synchronized之后,进入该方法,就会加锁,出了方法就会解锁

一个线程获取到🔒之后,除非他主动释放,否则不能强占

如果两个线程同时获取这个锁,只有一个线程能成功获取到,另一个没获取到的线程阻塞等待(BLOCKED)一直等待到上一个线程释放锁之后,当前线程才能获取到🔒

lock的阻塞就把刚才的t2的load推迟到t1的save之后了,也就避免了脏读问题!!(在t1执行提交数据后,t2再读数据)

再来看效果,多次执行后都是准确的结果,线程安全问题得到解决!!

加锁之后,代码的执行速度肯定会降低,但是为了主线任务,保证结果的正确性,是必须要加锁的

4.synchronized的使用方法

加锁要明确对哪个对象进行加锁,如果两个线程同时对一个对象加锁,就会产生阻塞等待(锁竞争/锁冲突),如果两个线程对不同对象加锁,不会产生锁竞争/冲突

4.1 修饰方法

修饰普通方法,进入方法加锁,出了方法解锁

锁的对象是this,哪个对象调用了方法,this就指向这个对象,即对这个对象加锁

这种情况就是把synchronized加到方法上了,相当于针对this加锁,当t1线程调用add()后,counter就加上锁了,另一个线程执行add()的时候,也尝试对counter 加锁,但是此时t1已经加过锁了,此时t2的加锁操作就会阻塞等待

修饰静态方法

锁的对象是类对象,和修饰普通方法同理

4.2 修饰代码块

需要显式指定锁对象,也就是手动指定🔒加到哪个对象上

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






原文链接: https://blog.csdn.net/chenchenchencl/article/details/128204672

标签: #软件开发 1171
相关文章

万字:支付“核心系统”详解 2024-11-02 15:33

专栏作者:隐墨星辰 \| 主编:陈天宇宙 这篇文章也尝试化繁为简,探寻支付系统的本质,讲清楚在线支付系统最核心的一些概念和设计理念。 虽然支付行业已经过了风头最劲的时光,但跨境支付仍然在蓬勃发展,每年依然有很多新人进入这个行业,这篇文章尝试为这些刚入行的新人提供一点帮助。 文章只介绍一些支付行业十几

资深支付架构师视角:实战从问题定义到代码落地的完整套路 2024-11-02 15:33

前言 今天从一个实际案例入手,介绍站在架构师的角度,如何识别并定义问题,提炼需求,技术方案选型,再到详细设计,最后利用AI的能力协助写出核心的代码,验证与调优。 解决问题存在一定的模式,也可以称之为框架,总结出自己的思考和解题框架,以后再碰到同类型的问题就可以如庖丁解牛一样容易。 很多年前,我写代码

Spring 实现 3 种异步接口 2024-10-18 09:07

大家好,我是苏三~ 如何处理比较耗时的接口? 这题我熟,直接上异步接口,使用 Callable、WebAsyncTask 和 DeferredResult、CompletableFuture等均可实现。 但这些方法有局限性,处理结果仅返回单个值。在某些场景下,如果需要接口异步处理的同时,还持续不断地

重学SpringBoot3-集成Redis(五)之布隆过滤器 2024-10-08 11:24

更多SpringBoot3内容请关注我的专栏:《SpringBoot3》 期待您的点赞👍收藏⭐评论✍ 重学SpringBoot3-集成Redis(五)之布隆过滤器 1. 什么是布隆过滤器? * 基本概念 适用场景 2. 使用 Redis 实现布隆过滤器 * 项目依赖 Redis 配置

设计模式第16讲——迭代器模式(Iterator) 2024-10-08 11:24

一、什么是迭代器模式 迭代器模式是一种行为型设计模式,它提供了一种统一的方式来访问集合对象中的元素,而不是暴露集合内部的表示方式。简单地说,就是将遍历集合的责任封装到一个单独的对象中,我们可以按照特定的方式访问集合中的元素。 二、角色组成 抽象迭代器(Iterator):定义了遍历聚合对象所需的方法

vue2路由和vue3路由区别及原理 2024-10-08 11:24

一、Vue2 与 Vue3 路由的区别 1. 创建路由实例方式的不同 Vue 2 中,通过 Vue.use() 注册路由插件,并通过 new VueRouter() 来创建路由实例。 import Vue from 'vue';import VueRouter from 'vue-router';i

目录

IT 外包服务商

  • 意见投递
  • zyf6619

软件开发应用

主菜单

  • 首页
  • 软件开发
  • 计算机基础
  • Hello Halo
  • 新手必读
  • 关于本知识库
Copyright © 2024 your company All Rights Reserved. Powered by Halo.