【Java多线程】线程安全问题与解决方案

news2025/1/16 16:05:11

目录

1、线程安全问题

1.2、线程安全原因

2、线程加锁

2.1、synchronized 关键字

2.2、完善代码

2.3、对同一个线程的加锁操作 

3、内容补充

3.1、内存可见性问题 

3.2、指令重排序问题

3.3、解决方法

3.4、总结 volatile 关键字

1、线程安全问题

  • 某个代码,无论是单线程下执行还是多线程下执行都不会产生bug,被称之为“线程安全”
  • 如果在单线程下执行正确,但是多线程下会产生bug,被称之为“线程不安全”或者“存在线程安全问题”

线程安全问题的典型例子

public class ThreadDemo {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        // 创建两个线程. 每个线程都针对上述 count 变量循环自增 50w 次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 500000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 500000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

        按照正常逻辑来看这段代码,结果应该是100w,但是通过多次运行发现这里给的却是一个50w到100w的随机值, 这就是因为出现了线程安全问题导致的结果错误。

问题分析:

count++操作实际上分成三步:

1)load 从内存中读取数据到cpu的寄存器

2)add 把寄存器中的值+1

3)save 把寄存器的值写回内存中

        而由于线程调度是随机调度,抢占式执行的,这就导致了两个线程的count++操作三步骤是会被打乱顺序的。

        例如 t1 线程先执行到 1)的同时, t2 线程也刚好随机调度开始执行 1),导致t1 和 t2 读取到的数据都是0,此时 t1 线程对 寄存器中值0+1,并将 1 写回内存中,接着 t2 也执行相同操作,再次将 1 写回内存中。此时就出现了线程安全问题,两次count++却只让count自增了1次。这就是这段代码为什么不是100w的原因细节。

1.2、线程安全原因

透过两个线程分别对count++,可以看到线程的不安全,有以下原因
1、根本原因,操作系统上线程的调度策略是“随机调度,抢占式执行”的,这就给线程之间执行的顺序带来了很多的变数。是线程安全问题的“罪魁祸首”。
2、代码结构问题,代码中多个线程同时修改一个变量。
3、直接原因,上述多线程修改操作,本身不是“原子的”,即实际count++操作又被分成了三步操作。如果操作本身是“原子的”,那么它要么执行,要么不执行,就不会出现执行一半,就被调度走,让其他线程“可乘之机”。

  • 针对原因1,系统底层对调度线程的逻辑就是随机调度,抢占式执行,无法干预做出任何调整。
  • 针对原因2,代码结构问题有时候是需求决定的,并不是每次都可以从这里入手,因此对于原因2也不好调整。
  • 针对原因3,既然操作非“原子的”,那么可以通过一些特殊手段将其打包成为“整体”,这也是我们接下来要讲到的加锁

2、线程加锁

2.1、synchronized 关键字

其中 locker 可以是任意对象,进入 synchronized 修饰的代码块, 相当于加锁,退出 synchronized 修饰的代码块, 相当解锁。

如果一个线程,针对一个对象加上锁之后,其他线程也尝试对这个对象加锁,就会导致锁竞争进而引起阻塞(BLOCKED),这个阻塞会一直持续到上一个线程释放锁为止。
如果是两个线程分别针对不同的对象进行加锁,此时不会由锁竞争,也就不会阻塞。

可以形象的理解成,每个对象在内存中存储的时候,都存有一块内存表示当前的 "锁定" 状态(类似于厕所的 "有人/无人")。

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

如果当前是 "有人" 状态,那么其他人无法使用,只能排队。

2.2、完善代码

public class ThreadDemo {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        // 随便创建个对象都行
        Object locker = new Object();

        // 创建两个线程. 每个线程都针对上述 count 变量循环自增 50w 次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 500000; i++) {
                synchronized (locker) {   //对count++进行加锁操作,打包三步为一步
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 500000; i++) {
                synchronized (locker) {   //对count++进行加锁操作,打包三步为一步
                    count++;
                }
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

通过对count++的整体加锁,使得每一次的count++都是一个整体,解决了此处的线程安全问题。

2.3、对同一个线程的加锁操作 

public static void main(String[] args) {
    Object locker = new Object();
    Thread t = new Thread(() -> {
        synchronized (locker) {
            synchronized (locker) {
                System.out.println("hello synchronized");
            }
        }
    });
    t.start();
}

需要注意的是,这里最直观的感觉是进行了两次加锁,会发生锁冲突。第一次针对locker加锁之后,在还没释放锁的时候又尝试对locker加锁,理论会出现锁冲突,但是这里却可以正常打印。

最关键的问题在于,【Java中的锁是可重入锁】这两次加锁,其实是在同一个线程中进行的,如果是同一个线程对同一个锁的多次加锁,是不会冲突的。

3、内容补充

当然,还有其他能够导致出现线程安全问题的原因:内存可见性问题以及指令重排序问题

3.1、内存可见性问题 

下列代码原本用意是:当用户输入非0数字时,结束线程t1。 

package thread;

import java.util.Scanner;

public class ThreadDemo {
    private static int flag = 0;   

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 0) {
                // 循环体里, 啥都不写会触发内存可见性问题
            }
            System.out.println("t1 线程结束!");
        });

        Thread t2 = new Thread(() -> {
            System.out.println("请输入 flag 的值: ");
            Scanner scanner = new Scanner(System.in);
            flag = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

实际运行结果时发现无法结束,t2修改了内存,但是t1内有看到这个内存的变化,就称之为“内存可见性”问题。出现这一问题是JVM的代码优化导致的。

t1 线程中的while语句每次循环是都有两个操作:

1、load 读取内存中 flag 的值到 cpu 寄存器中

2、拿到寄存器的值和 0 比较

上述循环中循环的执行速度非常之快,反复的执行1和2,即使是1秒也可能反复执行了几百万次。而在执行的过程中,有两个关键要点:

1、JVM识别到 load 操作执行的几百万次结果每次都一样(输入前的等待时间里)。

2、而由于 load 操作花费的开销远远超过剩余的其他操作(访问寄存器的操作速度远远超过访问内存)

每次循环可能就是百分之九十九的时间都消耗在 load 操作上,而百分之一的时间消耗在其他操作上,而且JVM发现每次 load 操作读取到的数据都是一样的,那么此时JVM就会认为此处每次 load 的操作是否有存在的必要呢,于是乎JVM就可能会自动执行了代码优化,将上述的 load 操作优化了(只有前几次进行了 load,后续发现 load 一直没有变化,分析代码也没发现哪里修改了flag,因此激进的将load操作优化成了直接使用寄存器中之前“缓存”的值),从而达到大幅度提高循环的执行速度的目的。

3.2、指令重排序问题

指令重排序也是编译器优化的一种方式。保证逻辑不变的前提下,调整原有代码的执行顺序,提高程序的效率。

3.3、解决方法

由于上述两种问题都是由于JVM代码优化导致的

Java提供的 volatile 关键字就可以使上述的优化被强制关闭,可以确保每次循环条件都会重新从内存中读取数据。
强制读取内存,虽然开销大了,效率也低了,但是数据的准确性、逻辑的准确性都提高了。

只需要对 flag 添加一个 volatile 关键字即可解决这一问题。

3.4、总结 volatile 关键字

1、保证内存可见性,每次访问变量必须都要重新读取内存,而不会优化到寄存器/缓存中
2、禁止指令重排序,针对被 volatile 修饰的变量的读写操作相关指令,是不能被重排序的。

【博主推荐】

【Java多线程】Thread类的基本用法-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136121421?spm=1001.2014.3001.5501 【Java多线程】对进程与线程的理解-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136115808?spm=1001.2014.3001.5501

【数据结构】二叉树的三种遍历(非递归讲解)-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136044643?spm=1001.2014.3001.5501

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

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

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

相关文章

雷卯有多种封装3.3V的ESD DIODE供您选择

一&#xff0e;3.3V ESD型号如下 二&#xff0e;多种多路封装 三&#xff0e;3.3V保护方案 方案优点&#xff1a;用于满足IC VCC 3.3V的静电浪涌保护&#xff0c;根据电源所处环境选择合适保护电流的ESD器件&#xff1b;高速传输接口选择超低电容ULC0511CDN&#xff0c;保证信…

友点CMS GetSpecial SQL注入漏洞复现

0x01 产品简介 友点CMS是一款高效且灵活的网站管理系统,它为用户提供了简单易用的界面和丰富的功能。无论是企业还是个人,都能通过友点CMS快速搭建出专业且美观的网站。该系统支持多种内容类型和自定义模板,方便用户按需调整。同时,它具备强大的SEO功能,能提升网站在搜索…

高并发系统中常见的问题

在当今的高并发系统中&#xff0c;常见的问题是多种多样的&#xff0c;这些问题往往会对系统的稳定性和性能产生重大影响。本文将详细介绍高并发系统中常见的问题&#xff0c;并探讨其产生原因和解决方案。 一、高并发系统概述 高并发系统是指在同一时间内有大量用户同时访问…

Sui在Dacade推出Move课程,完成学习奖励SUI

Dacade推出了一门Sui开发者课程&#xff0c;通过一系列引人入胜的挑战&#xff0c;为开发者提供了一个沉浸式的Move技术之旅。在这门课程中&#xff0c;Dacade的教育材料将引导用户利用Sui强大的DeFi原生功能&#xff08;包括DeepBook和zkLogin&#xff09;构建DeFi应用。此外&…

Tofu5m 高速实时推理Yolov8

Tofu5m 是高性价比目标识别跟踪模块&#xff0c;支持可见光视频或红外网络视频的输入&#xff0c;支持视频下的多类型物体检测、识别、跟踪等功能。 实测视频链接&#xff1a;Tofu5m识别跟踪模块_哔哩哔哩_bilibili 产品支持视频编码、设备管理、目标检测、深度学习识别、跟踪…

c++类和对象新手保姆级上手教学(中)

前言&#xff1a; 类和对象中篇&#xff0c;这里讲到的前4个默认成员函数&#xff0c;是类和对象中的重难点&#xff0c;许多资料上的讲法都非常抽象&#xff0c;难以理解&#xff0c;所以我作出这篇总结&#xff0c;分享学习经验&#xff0c;以便日后复习。 目录 6个默认成员…

基于uniapp微信小程序的汽车租赁预约系统

随着现代汽车租赁管理的快速发展&#xff0c;可以说汽车租赁管理已经逐渐成为现代汽车租赁管理过程中最为重要的部分之一。但是一直以来我国传统的汽车租赁管理并没有建立一套完善的行之有效的汽车租赁管理系统&#xff0c;传统的汽车租赁管理已经无法适应高速发展&#xff0c;…

【lesson59】线程池问题解答和读者写者问题

文章目录 线程池问题解答什么是单例模式什么是设计模式单例模式的特点饿汉和懒汉模式的理解STL中的容器是否是线程安全的?智能指针是否是线程安全的&#xff1f;其他常见的各种锁 读者写者问题 线程池问题解答 什么是单例模式 单例模式是一种 “经典的, 常用的, 常考的” 设…

MySQL多实例部署:从概念到实操的全面指南

目录 MySQL多实例管理 单实例 什么是多实例 多实例的好处 多实例的弊端 MySQL多实例用在哪些场景 资金紧张的公司 用户并发访问量不大的业务 大型网站也有用多实例 部署MySQL多实例 rpm和源码的优缺点 二进制方式安装mysql 准备二进制mysql运行所需的环境 准备多…

ENG-2 AM,绿色钠离子探针,能够与常用的显微镜和光谱仪兼容

文章关键词&#xff1a;ENG-2 AM&#xff0c;钠离子荧光探针ENG-2&#xff0c;绿色钠离子探针 一、基本信息 产品简介&#xff1a;Enhanced NaTrium Green-2 AM (ENG-2 AM) 是一种新型的钠离子&#xff08;Na&#xff09;荧光探针&#xff0c;它作为 Asante NaTrium Green-2 …

[Bug解决] Invalid bound statement (not found)出现原因和解决方法

1、问题描述 在写了一个很普通的查询语句之后&#xff0c;出现了下面的报错信息 org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.xxx.oauth.mapper.WxVisitorQrBeanMapper.selectByComIdAndEmpId at org.apache.ibatis.binding.Mappe…

力扣日记2.20-【回溯算法篇】491. 非递减子序列

力扣日记&#xff1a;【回溯算法篇】491. 非递减子序列 日期&#xff1a;2023.2.20 参考&#xff1a;代码随想录、力扣 ps&#xff1a;放了个寒假&#xff0c;日记又搁置了三星期……&#xff08;下跪忏悔&#xff09; 491. 非递减子序列 题目描述 难度&#xff1a;中等 给你一…

《Solidity 简易速速上手小册》第3章:Solidity 语法基础(2024 最新版)

文章目录 3.1 变量和类型3.1.1 基础知识解析详细解析变量类型深入数据类型理解变量可见性 3.1.2 重点案例&#xff1a;创建一个简单的存储合约案例 Demo&#xff1a;编写一个简单的数字存储合约案例代码&#xff1a;SimpleStorage.sol在 Remix 中进行交互&#xff1a;拓展操作&…

【python】linux系统python报错“ssl module in Python is not available”

一、问题现象 1.1 执行pip命令报错 pip安装时遇到openssl问题&#xff0c;没办法安装第三方库 “WARNING: pip is configured with locations that require TLS/SSL, however the ssl module in Python is not available. ” 1.2 导入import ssl 报错 直接执行python&…

算法面试八股文『 模型详解篇 』

说在前面 这是本系列的第二篇博客&#xff0c;主要是整理了一些经典模型的原理和结构&#xff0c;面试有时候也会问到这些模型的细节&#xff0c;因此都是需要十分熟悉的。光看原理还不够&#xff0c;最好是能用代码试着复现&#xff0c;可以看看李沐老师深度学习的教材&#…

AT24C02(I2C总线)通信的学习

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、存储器介绍二、AT24C02芯片二、I2C总线I2C电路规范I2C时序结构I2C数据帧AT24C02数据帧 总结 前言 学习AT24C02(I2C总线)芯片 一、存储器介绍 RAM&#xf…

PostgreSQL使用session_exec和file_fdw实现失败次数锁定用户策略

使用session_exec 、file_fdw以及自定义函数实现该功能。 缺陷&#xff1a;实测发现锁用户后&#xff0c;进去解锁特定用户。只能允许一次登陆&#xff0c;应该再次登陆的时候&#xff0c;触发函数&#xff0c;把之前的日志里的错误登陆的信息也计算到登录次数里了。而且foreig…

VsCode指定插件安装目录

VsCode指定插件安装目录 VsCode安装的默认目录是在用户目录(%HomePath%)下的.vscode文件夹下的extensions目录下&#xff0c;随着安装插件越来越多会占用大量C盘空间。 指定VsCode的插件目录 Vscode安装目录&#xff1a; D:\Microsoft VS Code\Code.exeVscode插件安装目录&a…

MyBatis数据库查询

文章目录 什么是MyBatisMyBatis程序的创建MyBatis实现数据库查询传参查询插入实现添加操作获取自增ID删除实现修改实现#{}和${}SQL注入 like查询 resultMap和resultType多表查询 对于普遍的后端开发而言&#xff0c;其程序主要包含了后端主程序和数据库两个部分&#xff0c;用户…

【Java】eclipse安装JDBC连接MySQL数据库教程

1.下载JDBC 首先在官网中下载JDBC&#xff0c;下载地址&#xff1a;MySQL :: MySQL Connectors 如果无法进入官网可以点击这里下载&#xff1a;https://download.csdn.net/download/weixin_47040861/88855729 进入官网后找到Develop by MySQL一栏&#xff0c;在该栏下找到JDB…