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

目录

一、线程不安全的原因

1、抢占式执行、随机调度

2、多线程同时修改同一个变量

3、修改操作不是原子的

4、内存可见性

5、指令重排序

二、解决方法

1、使用synchronized方法加锁

(1)、定义

(2)、使用

(3)、死锁

2、使用volatile关键字

3、使用wait和notify


一、线程不安全的原因

例:

class Counter{
    private int count=0;
    public void add(){
        count++;
    }
    public int get(){
        return count;
    }
}
public class demo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();
        //创建两个线程分别对counter自增50000次
        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();
        t1.join();
        t2.join();
        //结果不为100000!!!
        System.out.println(counter.get());
    }
}

两个线程针对同一个变量,各自自增50000次。预期结果是100000,实际结果却是个随机值,每次的结果都不同,这就是有多线程引起的线程线程安全问题。以下便是引起线程不安全的多个原因:

1、抢占式执行、随机调度

抢占式执行导致线程不安全的最根本的原因、是万恶之源。因为CPU在进行线程调度时,是随机的,线程中的代码执行到任意一行,都随时可能会被切换出去,因此无法解决。

2、多线程同时修改同一个变量

当多个线程同时修改同一个变量的时候,很容易产生线程的不安全。但可以通过调整代码结构来避免这个问题。

3、修改操作不是原子的

原子性是不可拆分的基本单位,因此如果修改操作是原子的,就不会出现太大问题,但如果是非原子的,出现问题的概率就非常大。

例:

count++操作,本质上是三个cpu指令构成

  • load,把内存中的数据读取到cpu寄存器中
  • add,把寄存器中的值,进行+1运算
  • save,把寄存器中的值写回到内存中

因为count++操作不是原子的,可以发生改变,所以上边的代码便出现了与预期不同的结果。因此针对这个问题,我们可以通过加锁(synchronized),使其变成一个整体。

4、内存可见性

一个线程频繁读,一个线程修改的操作也存在安全问题,可能产生脏读,读的结果不符合预期。

例:t1频繁读取内存,效率较低,于是就被优化成直接读取CPU寄存器;t2修改了内存的结果,由于t1没有读取内存,导致修改不能被识别到,因此产生了线程安全问题。

5、指令重排序

编译器在执行代码时会检测代码,存在编译器自作主张在保证相同的逻辑情况下对代码进行优化和修改,从而加快程序执行的效率,这是发生在单个线程里面的。这就有可能出现安全问题。

二、解决方法

1、使用synchronized方法加锁

(1)、定义

synchronized可以指定一个锁对象加锁,进入synchronized修饰的代码块, 相当于加锁,退出synchronized 修饰的代码块, 相当于解锁,不需要额外的解锁有效的防止遗忘解锁

例:线程1使用synchronized加锁,在线程1加锁的过程中synchronized会起到互斥效果,线程2无法把自己的指令插入到线程1的修改过程中,使线程1的加锁的修改操作变为原子性。

(2)、使用

class Counter{
    private int count=0;
    private Object locker=new Object();

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

    public void add(){
        //2、手动指定锁对象
        synchronized (locker) {
            count++;
        }
    }
    public int get(){
        return count;
    }
}
public class demo02 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();

        Thread t1=new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                counter.add();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.get());
    }
}

(3)、死锁

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

2、使用volatile关键字

线程修改一个变量,会把这个变量先从主内存读取到工作内存,然后修改工作内存的值,再写回到主内存中。

通过上文的内存可见性我们可以知道因为t1频繁读取主内存,效率较低,所以直接被优化成读取工作内存,当t2修改了主内存时,由于t1没有读取主内存,导致修改不能被识别到。

归根结底就是编译器在多线程环境下优化时产生了误判。此时,volatile这个关键字就可以发挥作用了。我们可以使用volatile关键字来修饰变量,来告诉编译器,这是一个易变的变量,不可以随意进行优化。以此保证内存可见性,禁止编译器优化。

public class demo {
    //此处,flag变量如果不被volatile修饰,程序就会一直运行,while感知不到flag的变化。
    volatile public static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (flag) { }
            System.out.println("t1线程结束!");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个false");
            flag = scanner.nextBoolean();
        });
        t1.start();
        t2.start();
    }
}

3、使用wait和notify

线程1调用wait方法后就会进入阻塞,此时处于WAITING状态。如果wait方法中没有参数,就一直等待,直到被notify唤醒。

注:wait的使用需要搭配synchronized。

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

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

相关文章

Github ChatGPT-Web:了解最新AI技术的前沿应用!

近年来OpenAI的ChatGPT模型在自然语言处理领域取得了很大的进展&#xff0c;并且已经在全球范围内得到了广泛的应用和普及。ChatGPT不仅可以用于生成对话和文本摘要等任务&#xff0c;还可以用于机器翻译、问答系统、情感分析等多个领域。ChatGPT已经成为自然语言处理领域的一个…

基于51单片机的自动打铃打鸣作息报时系统AT89C51数码管三极管时钟电路

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;单片机打铃 获取完整无水印论文报告说明&#xff08;含源码程序、电路原理图和仿真图&#xff09; 本次设计中的LED数码管电子时钟电路采用24小时制记时方式,本次设计采用AT89C51单片机的扩展芯片和6个PNP三极管做驱动&…

【C语言蓝桥杯每日一题】——跑步锻炼

【C语言蓝桥杯每日一题】—— 跑步锻炼&#x1f60e;前言&#x1f64c;排序&#x1f64c;总结撒花&#x1f49e;&#x1f60e;博客昵称&#xff1a;博客小梦 &#x1f60a;最喜欢的座右铭&#xff1a;全神贯注的上吧&#xff01;&#xff01;&#xff01; &#x1f60a;作者简介…

使用chatGPT实现数字自增动画

num-auto-add&#xff1a;数字自增动画 序言 我们经常在一些好的网站上遇到数字自增的动画效果&#xff0c;为用户提供了更加丰富的交互体验&#xff0c;看起来非常酷。 我之前也有写过&#xff0c;为了方便以后使用&#xff0c;打算将它优化&#xff0c;并上传到npm中。 首…

OpenCV入门(二十一)快速学会OpenCV 20 图像金字塔

OpenCV入门&#xff08;二十一&#xff09;快速学会OpenCV 20 图像金字塔1.基本概念2.高斯金字塔2.1 向下取样2.2 向上取样3.拉普拉斯金字塔作者&#xff1a;Xiou 1.基本概念 一般情况下&#xff0c;我们要处理的是一幅具有固定分辨率的图像。有些情况下&#xff0c;我们需要…

RabbitMQ 入门到应用 ( 六 ) 消息可靠性

7.RabbitMQ可靠性投递 为了保证信息不丢失, 可靠抵达,引入确认机制 消息从生产者传递到消费者的过程中, 不同的阶段使用不同的确认方式. 7.0.准备请求 一次性发送10 个消息 通过 new.exchange.direct交换机 接收消息, 使用 new.admin路由键 向 new.admin队列 发送消息. Aut…

【 构造 HTTP 请求 】

文章目录一、通过 form 表单构造 HTTP 请求1.1 form 发送 GET 请求1.2 form 发送 POST 请求二、通过 ajax 构造 HTTP 请求2.1 ajax 发送 GET 请求2.2 ajax 发送POST 请求2.3 关于 ajax三、通过 Java socket 构造 HTTP 请求(了解)一、通过 form 表单构造 HTTP 请求 form (表单)…

Element Plus 实例详解(六)___Progress 进度条

Element Plus 实例详解&#xff08;六&#xff09;___Progress 进度条 本文目录&#xff1a; 一、前言 二、搭建Element Plus试用环境 1、搭建Vue3项目&#xff08;基于Vite Vue&#xff09; 2、安装Element Plus 三、Element Plus Progress 进度条功能试用 1、直线进度条…

【数据结构】栈和队列

&#x1f680;write in front&#x1f680; &#x1f4dc;所属专栏&#xff1a;初阶数据结构 &#x1f6f0;️博客主页&#xff1a;睿睿的博客主页 &#x1f6f0;️代码仓库&#xff1a;&#x1f389;VS2022_C语言仓库 &#x1f3a1;您的点赞、关注、收藏、评论&#xff0c;是对…

血细胞智能检测与计数软件(Python+YOLOv5深度学习模型+清新界面版)

摘要&#xff1a;血细胞智能检测与计数软件应用深度学习技术智能检测血细胞图像中红细胞、镰状细胞等不同形态细胞并可视化计数&#xff0c;以辅助医学细胞检测。本文详细介绍血细胞智能检测与计数软件&#xff0c;在介绍算法原理的同时&#xff0c;给出Python的实现代码以及Py…

HTTP协议详解(上)

目录 前言&#xff1a; 认识URL HTTP协议方法 通过Fiddler抓包 GET和POST之间典型区别 header详解 HTTP响应状态码 常见状态码解释 状态码分类 HTTP协议报文格式 小结&#xff1a; 前言&#xff1a; HTTP协议属于应用层协议&#xff0c;称为超文本传输协议&#xff…

C++中的string类【详细分析及模拟实现】

string类 目录string类一、stirng的介绍及使用1.为什么学习string类&#xff1f;2.标准库中的string类2.1 引入&#xff1a;编码2.2 basic_string3.string类的使用3.1 构造函数3.2 遍历string方式1&#xff1a;for循环方式2&#xff1a;范围for4.迭代器4.1 正向迭代器4.2反向迭…

STM-32:按键控制LED灯 程序详解

目录一、基本原理二、接线图三、程序思路3.1库函数3.2程序代码注&#xff1a;一、基本原理 左边是STM322里电路每一个端口均可以配置的电路部分&#xff0c;右边部分是外接设备 电路图。 配置为 上拉输入模式的意思就是&#xff0c;VDD开关闭合&#xff0c;VSS开关断开。 浮空…

互联网数据挖掘与分析讲解

一、定义 数据挖掘&#xff08;英语&#xff1a;Data mining&#xff09;&#xff0c;又译为资料探勘、数据采矿。它是数据库知识发现&#xff08;英语&#xff1a;Knowledge-Discovery in Databases&#xff0c;简称&#xff1a;KDD)中的一个步骤。数据挖掘一般是指从大量的数…

多线程(四):线程安全

在开始讲解线程安全之前我们先来回顾一下我们学了那些东西了&#xff1a; 1. 线程和进程的认识 2. Thread 类的基本用法 3. 简单认识线程状态 4. 初见线程安全 上一章结束时看了一眼线程安全问题&#xff0c;本章将针对这个重点讲解。 一个代码在单线程中能够安全执行&am…

204. 计数质数 (埃式筛法详解)——【Leetcode每日一题】

素数最朴素判断思路&#xff1a;&#xff08;一般会超时&#xff09; 对正整数 n&#xff0c;如果用 2 到 n\sqrt{n}n​ 之间的所有整数去除&#xff0c;均无法整除&#xff0c;则 n 为素数又称为质数。 为什么到n\sqrt{n}n​ 就可以了&#xff0c;因为因数如果存在一定是成对…

【三】一起算法---栈:STL stack、手写栈、单调栈

纸上得来终觉浅&#xff0c;绝知此事要躬行。大家好&#xff01;我是霜淮子&#xff0c;欢迎订阅我的专栏《算法系列》。 学习经典算法和经典代码&#xff0c;建立算法思维&#xff1b;大量编码让代码成为我们大脑的一部分。 ⭐️已更系列 1、基础数据结构 1.1、链表➡传送门 1…

使用Node.js+Koa 从零开始写个人博客系统——后端部分(一)

使用Node.jsKoa 从零开始写个人博客系统系列 提示&#xff1a;在此文章中你可以学习到的内容如下&#xff1a; 1 如何使用Koa快速搭建项目 2 对Koa的核心组件Koa-Route的简单使用 3 3层架构思想 4 nodejs的ORM框架——sequelize的使用 5 sequelize-auto的使用 6 简单的增删查改…

【蓝桥杯嵌入式】第十三届蓝桥杯嵌入式国赛客观题以及详细题解

题1 概念题。 USRAT&#xff1a;异步串口通信&#xff0c;常用于数据传输&#xff1b;SW-DP&#xff1a;SWD 的全称应该是 The Serial Wire Debug Port (SW-DP),也就是串行调试端口&#xff0c;是 >ARM 目前支持的两种调试端口之一&#xff1b;JTAG-DP&#xff1a;另一个调试…

git基本用法教程(fork软件+git命令)

git基本用法教程1. git commit2. git branch3. git checkout4. git merge5. git rebase6. 在提交树中移动7. 撤销变更8. 整理提交记录9. 提交的技巧10. git clone11. git push12. git pull13. git fetch14. git flow15. git stash16. fork的使用当然除了环境和demo的运行和改写…