Java线程安全问题的原因和解决方案

news2025/1/19 8:26:44

  • 1.什么是线程安全
  • 2.线程不安全的原因 及 解决措施
    • 2.1 多线程同时修改同一个变量
    • 2.2 修改操作不是原子性
      • 加锁操作关键字:`synchronized`
    • 2.3 抢占式执行,随机调度 (根本原因)
    • 2.4内存可见性问题
      • volatile 关键字
    • 2.5指令重排序

1.什么是线程安全

线程安全的确切定义是比较复杂的,不过我们可以这样认为:当多线程环境下的代码运行的结果是符合我们预期的,即在单线程环境下应该得到的结果,则说这个程序是线程安全的,反之,则是线程不安全.

注意:判定一个代码是否线程安全,要具体问题具体分析,不是加了锁就一定安全~

2.线程不安全的原因 及 解决措施

2.1 多线程同时修改同一个变量

如果一个线程修改一个变量;多线程读取同一个变量,是安全的,修改不同变量,都是安全的。因此可以通过调整代码结构来避免这种问题。

  • 来看一段带有线程安全问题的代码

    class Counter{
        public  int count;
        public void add(){
            count++;
        }
    }
    public class ThreadDemo10 {
        public static void main(String[] args) {
            //1.定义Counter实例
            Counter counter = new Counter();
            //2.定义两个线程,分别对counter 调用5w次的add方法 预期结果count = 10 0000
            Thread t1 = new Thread(()->{
                for (int i = 0; i < 50000; i++) {
                    counter.add();
                }
            });
            t1.start();
            Thread t2 = new Thread(()->{
                for (int i = 0; i < 50000; i++) {
                    counter.add();
                }
            });
            t2.start();
            //3.等待两个线程结束
            try {
                t1.join();
                t2.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //打印最终的count值
            //输出的结果是无法确定的
            System.out.println("count = "+counter.count);
        }
    }
    

在这里插入图片描述

  • 此时输出的结果是和预期结果不同,这个现象称为bug。产生这个结果最根本的原因就是抢占式执行,随机调度。同时这也是个典型的线程安全问题。

  • 为什么出现这种情况?

    count++ 的++操作本质是要分成 三步走:

    ​ 1.把内存中的值,读取到cpu的寄存器中。(load)

    ​ 2.把寄存器中的值进行+1操作。(add)

    ​ 3.把得到的结果写回内存中。(save)

    (注意:这里的load add save 是cpu指令)

    此处这段代码 是两个线程针对同一个count++,也就是有两组load add save。如果线程调度顺序不同,执行的顺序也就不同,导致最终结果就不同。

    此处画的调度顺序只有6种,实际有无数种情况:

在这里插入图片描述

比如我们拿最后一组执行顺序来说明,t2先执行了load 把count加载到cpu寄存器中,执行add 把count进行+1操作,此时t1执行load 把count加载到cpu寄存器中(此时count依旧为0,t1读取到的count值为0;因为t2还没有进行save把count=1写回内存中),t1执行add把count进行+1操作,t1执行save把count写回内存,此时内存中count值为1;最后t2执行save(此时count在寄存器中的值是1)把count=1写回内存中,最终两个线程对count进行两次自增操作,实际上count经过两次++操作 只增加了一次。

2.2 修改操作不是原子性

  • 概念:

修改操作不是原子性也就意味着在执行任务时,没有做完,执行中途 被调度走了(原子:即不可拆分的基本单位,对于上面的列子,对于count++就不是原子的,可以拆分成load、add、save,像单独一个load或者add和save这就是不可再拆分的,是原子的)。如果操作是原子性的,就不会有线程不安全的问题了。

  • 解决措施:通过加锁,把不是原子的变成原子(线程不安全的最根本解决措施)

加锁操作关键字:synchronized

把count的add方法加锁:意味着进入add方法就被自动加锁,出了方法就自动解锁。那么如果两个线程同时尝试加锁,A线程成功获取到锁,B线程就会一直阻塞等待(BLOCKED状态),一直等到A线程解锁,B才会成功获取到锁(才能执行代码)。

class Counter{
    public  int count;
    //add方法进行加锁
   synchronized public void add(){
        count++;
    }
}

在这里插入图片描述

  • 锁对象规则

1): 如果两个线程A,B线程针对同一个对象进行加锁操作,就会出现锁冲突(锁竞争),A线程获取到锁(讲究先到先得),B线程阻塞等待,等待A线程解锁,B才能获取成功.

2): 如果此时针对不同的对象加锁,就不会出现锁冲突,因为这两个线程获取的是不同的锁,就不会有阻塞等待了.

3): 如果两个线程一个加锁一个不加锁,也是没有锁冲突的.

  • synchronized使用方法:
  1. 修饰方法:进入方法就加锁,离开方法就解锁。

​ a)修饰普通方法 加锁对象是this

​ b)修饰静态方法 加锁对象是类对象

  1. 修饰代码块:手动指定锁对象

进入代码块就加锁,出了代码块就解锁。括号里的对象可以任意指定其他对象。

    public void add(){
        synchronized(this){
            count++;
        }
    }
  • 可重入锁synchronized

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

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

上述代码,一个线程当进入add方法时,就会加锁(这次能够加锁成功),随后进入代码块,再次尝试加锁,此时站在锁对象(this)来看,它认为自己已经加锁了,这里要不要再进行加锁?还是进行阻塞等待?如果进行加锁,就是可重入锁,如果进行阻塞等待,就是不可重入锁,如果此处进行阻塞等待,那么就成了死锁(没法解锁)。

死锁: 此处A线程第一次加锁成功,如果有其他线程获取锁就会进行阻塞等待,可是A线程在第二个锁处进行阻塞等待了,由于其他线程都阻塞在第一个锁外,A线程又无法把第一把锁进行解锁,其他线程也进不来,就成了死锁。(抽象了 = =简单讲就是A加锁了,又卡在另一把锁,另一把锁也没线程来解锁,就死锁了)

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

​ 万恶之源,罪魁祸首!操作系统调度线程是具有随机性的,多线程环境下会出现抢占式执行,此时就会出现代码执行顺序的可能性从一种情况变成了无数种情况,所有就需要保证在这种随机的线程调度顺序下,保证结果都是正确的预期结果.

  • 解决措施:wait 和 notify

比如t1先执行,t2先wait(阻塞,主动放弃cpu),等t1执行差不多了,再通过notify把t2唤醒,让t2执行。

如果使用join,则必须要t1彻底执行完,t2才能执行,sleep必须指定一个固定休眠时间,但实际执行多久我们并不确定,而wait和notify可以随便指定执行到何等程度时把t2唤醒或者让t2阻塞,控制多线程的执行顺序。

方法说明
wait() / wait(long timeout)让当前线程进入等待状态,此时处在WAITING状态。wait()放不加任何参数,就是一直等待。
notify()唤醒在当前对象上等待的线程
notifyAll()唤醒所有等待的线程

注意: wait, notify, notifyAll 都是 Object 类的方法 。

public class ThreadDemo12 {
    public static void main(String[] args) throws InterruptedException {
        Object ob = new Object();
        ob.wait();
    }
}

在这里插入图片描述

wait会做三件事:

  1. 先释放锁。(因此线程在没有加锁情况下进行wait会抛异常,所以要配合synchronized使用)
        synchronized (ob) {
            //此时这个线程是阻塞在wait这一行代码处,等待notify唤醒
            //虽然是阻塞,带实际上是释放了锁,其他线程可以获取到ob对象这个锁
            ob.wait();
        }
  1. 进行阻塞等待。

  2. 收到通知后,重新尝试加锁,获取到锁后,继续往下执行。

  • 代码实例:解决抢占式执行,按照我们的意愿有顺序的执行
public class ThreadDemo13 {
    public static void main(String[] args) {
        Object object = new Object();
        Thread t1 = new Thread(()->{
           //这个线程负责等待
            System.out.println("t1:wait 之前");
            synchronized (object){
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1:wait 之后");
            }
        });

        Thread t2 = new Thread(()->{
            //这个线程负责通知唤醒
            System.out.println("t2: 通知之前");
            synchronized (object){
                object.notify();
            }
            System.out.println("t2: 通知之后");
        });
        t1.start();
        //为了保证t1先执行
         try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t2.start();
    }
}

在这里插入图片描述

  1. 注意此处wait和notify使用的对象必须是同一个对象,如果是不同的对象,则此时notify不会有任何效果;notify只能唤醒在同一个对象上的等待的线程。
  2. 此处t1和t2的start,由于线程调度的随机性,不能保证是先wait后执行notify,如果先执行notify后执行wait,则此处wait就无法被唤醒,所以在t2.start()之前先sleep一段时间,保证先后顺序。

2.4内存可见性问题

比如一个线程读操作,一个线程改,就可能会出现读操作的结果不符合预期,也就是读到的值可能不是修改后的值。

解决方案:保证内存可见性

volatile 关键字

volatile关键字能保证内存的可见性,不保证原子性。

class Number{
    int flag = 0;

}
public class ThreadDemo11 {
    public static void main(String[] args) {
        //创建number
        Number number = new Number();

        Thread t1 = new Thread(()->{
            //循环读取flag值
            while(number.flag == 0){
                ;
            }
         System.out.println("t1线程结束");

        });
        t1.start();

        Thread t2 = new Thread(()->{
            //写操作 修改flag值
            Scanner scanner = new Scanner(System.in);
            number.flag = scanner.nextInt();
        });
        t2.start();
    }
}

在这里插入图片描述

上述代码读写同一个变量,当t2进行写操作,t1循环获取flag值。预期结果是:t2输入的是非0,则t1的循环结束。实际情况是:当输入1的时候,t1线程一直未结束。这种情况就属于内存可见性问题。

t1里的while循环的条件判断语句 用汇编层次来看,有两步操作1、load 把内存中的flag值读到寄存器中,2、cmp 把寄存器的值和0比较,决定下一步怎么走(条件跳转指令)。注意此处while循环体没有语句,也就意味着while循环执行速度极快,可能1s上万次,而执行load执行效率速度太慢,所以当执行这么多次每次load的值都是相同的,于是JVM就进行自动优化,导致只读取一次flag值,每次比较就不再读取flag值,直接用寄存器之前存储的值来和0比较。但实际上我们有其他线程随时对flag进行修改,这就导致JVM自动优化导致了错误。也就是前面说的一个线程读,一个线程改,当修改后,读的可能就不是修改后的值。要想解决这个问题,就需要我们手动对flag这个变量加上volatile关键字,加上之后编译器就不会进行优化,一定是每次都重新读取flag值。

class Number{
    //加上volatile关键字
   volatile int flag = 0;

}

在这里插入图片描述

此时给flag加上volatile关键字后,当t2输入非0时,t1的while循环就成功结束了。注意:编译器优化是由于t1循环里没有语句,循环速度极快才导致了优化,如果在循环里加个sleep控制循环速度,即使不加volatile,该代码执行结果也会正确(t1循环结束)。

2.5指令重排序

编译器觉得你的代码不够效率,于是把你的代码在保证逻辑的不变的情况下,进行调整(调整代码执行顺序),以加快程序的执行效率。
比如你写的代码执行顺序是1、2、3,编译器给你优化成1、3、2。

  • 解决措施:

由于编译器认为我们的代码不够效率,进行了指令重排序(优化),那么我们可以告诉编译不要优化,比如上面的volatile关键字。

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

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

相关文章

Java——SSM项目(瑞吉外卖)笔记

阅读提醒&#xff1a;最重要的内容都是我手打的字&#xff0c;还有截图上的红字备注部分。 nginx是一个服务器&#xff0c;主要部署一些静态的资源&#xff0c;包括后面做tomcat的集群&#xff0c; 可以接收前端的请求&#xff0c;然后分发给各个tomcat 第一步搭建数据库&…

浏览器网页视频怎么快速下载到本地?

我们在浏览网页时&#xff0c;经常会遇到一些特别喜欢的视频文件&#xff0c;想要下载收藏却苦于不会操作怎恶魔办呢&#xff1f;这时候可以通过一些小插件快速达成下载&#xff0c;比如通过猫爪视频下载插件用户可以轻松的抓取任意网页的视频文件&#xff0c;并将其保存到本地…

Java 利用PriorityQueue进行无InvokerTransformer反序列化

java_PriorityQueue java.util.PriorityQueue 是一个优先队列&#xff08;Queue&#xff09;&#xff0c;节点之间按照优先级大小排序成一棵树。其中PriorityQueue有自己的readObject反序列化入口。 反序列化链为&#xff1a;PriorityQueue#readObject->heapify()->sif…

新网站沙盒期要多久(关于网站走出沙盒期的征兆)

做网站优化首先要明白搜索引擎抓取原理&#xff0c;不管是百度还是谷歌&#xff0c;新站上线总要进入沙盒&#xff0c;接受来自搜索引擎的审查&#xff0c;涉及网站结构、网站内容、网站外链等内容。对于新手朋友来说&#xff0c;难免着急&#xff0c;这段考察期究竟有多长&…

【Python获取相亲网站数据】马上都元宵节了,还在相亲,看看某相亲网站有没有那个有缘人。

前言 马上都元宵节了&#xff0c;还在相亲&#xff0c;看看某相亲网站有没有那个有缘人。今天我们来爬取某相亲网站获取我们想要的数据&#xff0c;比如说&#xff0c;对方的姓名&#xff0c;年龄&#xff0c;身高&#xff0c;体重等等。今天我们主要使用CSS选择的方法来匹配我…

IDEA插件开发入门.01

环境准备Idea插件SDK文档在线地址&#xff1a;https://plugins.jetbrains.com/docs/intellij/welcome.html安装IntelliJ IDEA&#xff0c;这里使用版本2020.1.3 X64IDEA中安装Plugin DevKit插件创建插件项目新建工程。File ->New -> Project选择工程类型&#xff0c;Inte…

无法应用转换程序。请检查指定的转换程序路径是否有效。例子:Adobe Acrobat DC (PDF编辑器)卸载不了或者无法重新安装

不知道大家有没遇到这种情况&#xff0c;Adobe Acrobat DC (PDF编辑器)卸载不了或者无法重新安装&#xff0c;显示&#xff1a;无法应用转换程序。请检查指定的转换程序路径是否有效。 今天小编句遇到了这种情况&#xff0c;卸不了&#xff0c;把文件夹直接删了还是无法重新安装…

Linux安装Mysql8.0

mysql官网 www.mysql.com 这里是新建了个虚拟机 有时候用 rpm -qa|grep mysql和 rpm -qa|grep mariadb检测不到已经安装了mysql或者mariadb 可以使用rpm -qa|grep -i mysql 自己对Linux学习阶段,因此新建虚拟机安装 卸载原来的mariadb rpm -e mariadb-libs rpm -e --node…

微信如何注册小号?一个手机号注册两个微信账号?图文教学

2023年2月3日微信正式开放注册“小号”的功能&#xff0c;也就是可以使用一个手机号来注册两个微信账号。微信作为很多一款国民级别的工具&#xff0c;早就成为了小伙伴日常生活中不可或缺的一部分了。能够注册微信小号自然很好&#xff0c;可是微信如何注册小号呢&#xff1f;…

什么是窜货?为什么要治理窜货?如何正确治理窜货?

一、什么是窜货 “窜货”行为&#xff0c;就是超出自己授权范围&#xff0c;在非授权区域出售商品的行为。一般有线下窜区域销售、线下商品窜货线上销售两种情况。 二、为什么会出现窜货 1、窜货最常见的原因是&#xff0c;当经销商的授权区域市场趋向饱和时&#xff0c;或其…

茶杯:N个杯子排成一排,第X个杯子藏有球,交换任意两个杯子K次后,找出藏球杯子位置。

N个杯子排成一排&#xff0c;第X个杯子藏有球&#xff0c;交换任意两个杯子K次后&#xff0c;找出藏球杯子位置。 (本文获得CSDN质量评分【85】)【学习的细节是欢悦的历程】Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖免费“圣经”教程《 python 完…

Idea JSP 学习

运行JSP文件首次使用Idea运行jsp文件遇到的问题汇总。运行出现404错误。首先配置好Tomcat,我选用的是Tomcat 10.0.27.安装好Tomcat,并进行相关配置。我用的是IDEA2020.2.3 x64版本&#xff0c;不同版本会有区别。这是我的以上配置&#xff0c;特别注意下图三角标识的test.jsp。…

【JavaEE】单例模式详解

目录 一、单例模式的概念 二、饿汉模式 三、懒汉模式 &#xff08;1&#xff09;懒汉模式-单线程版 &#xff08;2&#xff09;懒汉模式-线程安全多线程版 啥是设计模式咧&#x1f914;设计模式好比象棋中的棋谱。红方当头炮&#xff0c;黑方马来跳&#x1f463;。针对红方…

鸿蒙开发学习|HarmonyOS是什么

鸿蒙开发学习 第一章 HarmonyOS是什么文章目录鸿蒙开发学习前言一、什么是HarmonyOS二、HarmonyOS系统架构2.1 内核层2.2系统服务层2.3框架层2.4应用层三、HarmonyOS系统特性3.1 对消费者3.2.对应用开发3.3.对设备开发四、HarmonyOS系统四大技术特性4.1 分布式架构首次用于终端…

LabVIEW最大内存块属性不存在

LabVIEW最大内存块属性不存在在NI Linux实时操作系统目标中使用系统属性节点和分布式系统管理器&#xff08;DSM&#xff09;&#xff0c;但遇到一些问题&#xff1a;它未正确报告系统上的可用物理内存量。在NI Linux实时系统上出现错误-2147220623尝试在NI Linux实时上监测RAM…

webpack自动化打包

webpack自动化打包 首先下载包 npm i webpack-dev-server -D 配置 webpack.config.js const path require(path)//nodejs核心模块&#xff0c;专门用来处理路径问题 const ESLintPlugin require(eslint-webpack-plugin) const HtmlWebpackPlugin require(html-webpack-p…

java-加密、解密算法

rsa2048、sha256 rsa2048对整个文件进行hash算法&#xff0c;生成公钥、私钥后用于数字签名&#xff0c; sha256通过公钥和私钥&#xff0c;作为证书使用。单板打包后对每个动态库sha256计算颁发证书 sha256签名后&#xff0c;rsa2048进行加密。签名用于校验加密数据没有被更…

初始化一个vite+vue3项目,配置eslint+Prettier

引用vite官网的一段话&#xff0c;解释一下我们为什么要选择使用vite 时过境迁&#xff0c;我们见证了诸如 webpack、Rollup 和 Parcel 等工具的变迁&#xff0c;它们极大地改善了前端开发者的开发体验。 然而&#xff0c;当我们开始构建越来越大型的应用时&#xff0c;需要处…

Java后端数据校验学习总结

Java后端校验总结 后端校验注解一直在用&#xff0c;但是感觉不是特别清楚&#xff0c;希望通过写这篇文章搞清楚。 Spring自带的Validation校验框架 Spring提供了Validator接口来校验对象&#xff0c;主要涉及到的方法和类如下&#xff1a; supports方法&#xff1a;设置校…

win11任务栏图标闪烁|任务栏QQ图标闪动|新消息任务栏自动弹出|设置自动隐藏任务栏之后,QQ或微信等工具新消息自动弹出任务栏并颜色提示问题解决方案

背景介绍: 今天正常使用电脑时也出现消息弹出问题(已经设置隐藏任务栏),很头疼那么时什么情况,该如何组去解决呢?(微信任务栏闪动未读消息) MyDockFinder Windows 桌面美化工具 目录 背景介绍 解决问题 微信环境测试 初始界面&#xff08;微信&#xff09; 打开微信 …