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

news2024/11/29 22:35:35

文章目录

  • 一、什么是线程安全问题
  • 二、出现线程安全问题的原因
  • 三、解决方案
    • 3.1加锁

一、什么是线程安全问题

某些代码在单线程环境下执行结果完全正确,但在多线程环境下执行就会出现Bug,这就是“线程安全问题”。
下面以一个变量n自增两次,每次自增10000为例。
单线程环境下n自增两次结果为20000,执行结果正确。

public class Demo4 {
    private static int n = 0;
    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            n++;
        }
        for (int i = 0; i < 10000; i++) {
            n++;
        }
        System.out.println(n);
    }
}

在这里插入图片描述
但在多线程环境下n自增两次,结果却不是20000,此时出现了Bug,出现了线程安全问题。

public class Demo1 {
    private static int n = 0;//n要定义成全局变量
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                    n++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                    n++;
            }
        });
        t1.start();
        t2.start();
        t1.join();//join一定要有,因为没有join的话t1和t2两个线程中count还没有自加主线程就已经打印n了
        t2.join();
        System.out.println(n);
    }
}

在这里插入图片描述
在上诉例子中出现线程安全问题的原因是:
n++这个操作本质上是分成三步执行的,有cpu的三个指令完成这三步。这三步为
①load,把数据从内存读取到cpu寄存器中
②add,把cpu寄存器中的数据+1
③save,将寄存器中的数据再保存到内存中
比如n原始值为0,线程1有一个n++,线程2有一个n++,线程1和线程2并发执行,n最后的结果应该为2,但实际上并非如此。cpu的一个核上先执行线程1的load,再执行线程2的load、add、save,此时内存中的n变为1,最后执行线程1的add、save,此时内存中的n仍然为1。
这说明由于线程之间的调度顺序是随机的,前一个线程的这三个步骤和后一个线程的这三个步骤可能会穿插执行,这时就会出现“线程安全问题”。

二、出现线程安全问题的原因

①操作系统调度线程的顺序是随机的(抢占式执行)。
②两个线程针对同一个变量进行修改。
③修改操作是非原子性的,如n++分为三个步骤,是非原子性的操作。类似地如果一段逻辑中需要根据一定的条件来决定是否修改,也会存在类似的问题。
④内存可见性问题。比如当t1线程有一个循环速度很快的循环,同时这个循环每循环一次要读一次内存,这个短时间内多次读内存的操作开销很大。在这个短时间内,编译器/JVM发现每次从内存中读到的结果是一样的,就做了一个大胆的决定,既然每次从内存中读到寄存器中的数据是一样的,那么这个循环就不需要每循环一次就读一次内存了,第一次从内存中读数据到寄存器上后,以后的循环就读寄存器中的值,不再关心内存中的值了。这是编译器针对短时间内多次读内存操作开销很大的一个优化,提高了程序整体的效率,也可以说是编译器的Bug。
在多线程环境中,t2线程修改了内存中的值,此时t1线程不会感受到内存中数据值的变化,就会造成线程安全问题的出现。
⑤指令重排序问题。比如在下面的代码中:

private static SingletonLazy instance = null;    
public static SingletonLazy getInstance() {        
    if (instance == null) {            
        synchronized (SingletonLazy.class) {                
            if (instance == null) {                    
                instance = new SingletonLazy();                
           }            
        }        
    }        
    return instance;   
}    

上面代码创建对象的new操作,是有可能触发指令重排序的。
创建对象的new操作大概可以分成三步:
①申请内存空间。
②在内存空间上创建对象(调用构造方法)。
③把内存的地址赋值给instance引用。
可以安装①②③的顺序执行,也可以按照①③②的顺序执行,①一定是首先执行的。
假设t1按①③②的顺序执行,当t1执行完①③的时候,此时instance就已经是非空的了,但是此时instance指向的是还没有初始化的非法对象。此时t2开始执行第一个if判断(t2的第一个if语句不涉及任何加锁操作,这个if是完全可以执行的,锁的阻塞等待,是只有两个线程的某个部分都加上同一把锁的情况下才会触发。这里t1创建对象的new操作加了一把锁,t2的第一个if语句操作没有加锁,这两者是可以并发执行的),判断到instance不为空,直接返回了instance。进而t2线程的代码就可能会访问到inStance里面非法的内容。

三、解决方案

依据以上造成线程安全问题的五个原因依次给出相应的解决方案。
问题①是系统内核调度方式造成的,很难解决。
有些情况下可以通过调整代码解决问题②,但是也有很多情况因为需求要求而调整不了。
问题③是可以解决的,比如上面的例子让n++的三个步骤变为原子性(三个步骤要么全部一起执行,要么一个也不执行),即加锁。
对于问题④,内存中的值比如可以是全局变量中存储的值,此时用volatile关键字修饰这个全局变量,就可以让编译器停止对这个全局变量进行内存可见性相关的优化,内存可见性问题也就解决了。
对于问题⑤,可以给inStance加上volatile修饰,这样就可以保证inStance在被修改的过程中不会发生指令重排序了。这保证了t2的第一个if在判断inStance是否为空时,inStance为非空时一定是指向了一个已经初始化的合法对象。

3.1加锁

通过synchronized对代码进行加锁。synchronized在使用的时候要搭配一个代码块,比如:

synchronized () {
    
}

在代码块里写上n++。

synchronized () {
   n++;
}

进入{会加锁,出了}会解锁,在前一个线程已经加锁的状态中,后一个线程尝试同样加这个锁就会产生“锁冲突/锁竞争”,后一个线程就会阻塞等待一直等到前一个线程解锁为止(这也是synchronized的特性:互斥)。当前一个线程解锁时前一个线程中的n++的三个步骤已经执行完毕,避免了前一个线程的三个步骤和后一个线程的三个步骤穿插执行的现象,实现了“串行执行”的效果,解决了线程安全问题。
那么我们怎样去区分两个线程加的锁是不是同一把锁呢?从上面的代码看出,synchronized关键字的后面有一个(),在这个()中传入一个对象,这个对象可以是随便一个对象,这个对象是谁不重要,重要的是我们可以通过这个对象来区分两个线程加的锁是不是同一把锁。
以下代码是两个线程加的锁是同一把锁,两个线程()里传的是同一个对象lock1。

Object lock1 = new Object();
Thread t1 = new Thread(() -> {
    for (int i = 0; i < 10000; i++) {
        synchronized(lock1) {
            n++;
        }
    }
});
Thread t2 = new Thread(() -> {
    for (int i = 0; i < 10000; i++) {
        synchronized(lock1) {
            n++;
        }
    }
});

以下代码是两个线程加的锁不是同一把锁,一个线程()里传的对象是lock1,另一个线程()里传的对象是lock2。

Object lock1 = new Object();
Thread t1 = new Thread(() -> {
    for (int i = 0; i < 10000; i++) {
        synchronized(lock1) {
            n++;
        }
    }
});
Thread t2 = new Thread(() -> {
    for (int i = 0; i < 10000; i++) {
        synchronized(lock2) {
            n++;
        }
    }
});

像下面那样对两个线程的n++加上同一把锁之后,线程安全问题就不会出现了,执行结果依然像单线程那样为20000。

public class Demo1 {
    private static int n = 0;//n要定义成全局变量
    public static void main(String[] args) throws InterruptedException {
        Object lock1 = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronized(lock1) {
                    n++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronized(lock1) {
                    n++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();//join一定要有,因为没有join的话t1和t2两个线程中count还没有自加主线程就已经打印n了
        t2.join();
        System.out.println(n);
    }
}

在这里插入图片描述
synchronized除了修饰代码块之外,还可以修饰一个实例方法或者静态方法。以下为示例代码:

class Counter {
    public static int count;
    //法一
    synchronized public void increase1() {
        count++;
    }
    //法二
    public void increase2() {//法二是法一的简化写法
        synchronized (this) {//对当前对象加锁
            count++;
        }
    }
    //法三
    synchronized public static void increase3() {
        count++;
    }
    //法四
    public static void increase4() {//法四是法三的简化写法
        synchronized (Counter.class) {//对类对象加锁
            count++;
        }
    }
}
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase4();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase4();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

synchronized的特性除了“互斥”之外,还有“可重入”。
“可重入”指的是一个线程针对同一把锁连续加锁多次时不会出现“死锁”。
观察以下代码:

synchronized (locker) {

    synchronized(locker) {
    
    }
    
}

第二次要想加锁成功,就需要第一次加的锁进行释放。第一次加的锁要想进行释放,又必须要让第二次加锁能够成功。由于第二次加锁阻塞住了,导致第一次加的锁无法释放,出现“死锁”的情况(线程卡死了)。
由于Java中synchronized“可重入”的特性,有效地解决了上诉“死锁”的问题。
“可重入锁”包含“线程持有者”和“计数器”两个信息:①如果某个线程加锁的时候, 发现锁已经被某个线程占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁并加锁, 并让计数器自增。每加锁一次计数器加1,每解锁一次计数器减1。②解锁的时候计数器递减为0时才真正释放锁。

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

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

相关文章

Linux【一】

目录 一、Linux操作系统发展历史 Unix Minix Linux 二、Linux简介 Linux是什么 Linux的版本 Linux内核版本 Linux发行版本 Linux应用领域&#xff1f; Linux注意事项 三、Linux目录 系统目录 用户目录 文件颜色 四、Linux命令行基本操作 Linux命令格式&#x…

小程序中会员如何绑定身份证信息

在小程序中&#xff0c;会员绑定身份证信息是一项重要且常见的功能&#xff0c;可以帮助商家更好地了解用户的身份信息&#xff0c;提供个性化的服务和保障用户的权益。例如生日礼物&#xff0c;还有以及医保支付等需要实名认证的场景等。下面将介绍一些小程序中会员如何绑定身…

Open Feign

Open Feign 在前面的学习中&#xff0c;我们使用了Ribbon的负载均衡功能&#xff0c;简化了远程调用时的代码&#xff1a; String user this.restTemplate.getForObject("http://spring-provider/provider/" id, String.class);如果就学到这里&#xff0c;可能以…

Zebec 生态 AMA 回顾:Nautilus 以及 $ZBC 的未来

在 9 月 7 日&#xff0c;Zebec 创始人 Sam 做客社区&#xff0c;并进行了“Nautilus Chain 以及 $ZBC 的未来”主题的 AMA 访谈。Sam 在本次访谈中对 Nautilus Chain 生态的价值捕获、Zebec 生态布局规划、可能会推出的 Nautilus Chain 治理通证 NAUT 进行了解读。本文将对本次…

基于SSM的医院在线挂号预约系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

人大女王金融硕士项目——应以长远的眼光去规划自己的人生蓝图

我们的现在的生活都是过去努力的结果&#xff0c;你的未来如何是由今后的努力决定。我们不要停滞不前&#xff0c;应从长远的眼光去规划自己的人生蓝图。你有想过在职攻读人民大学与加拿大女王大学金融硕士项目来提升自己吗&#xff1f; 着我国经济迅猛的发展&#xff0c;金融…

鸿蒙HarmonyOS应用开发初体验

最近华为发布mt60新机火了&#xff0c;作为一名移动开发程序员&#xff0c;对鸿蒙系统开发移动端就很感兴趣了。 开发工具&#xff1a;HUAWEI DevEco Studio和SDK下载和升级 | HarmonyOS开发者 下载完后按默认安装就可以了&#xff0c;界面跟AS很类似&#xff0c;之前我jdk环…

前端面试合集(二)

前端面试题合集 1.懒加载的原理及实现了解吗2.如何理解JS异步3.阐述一下 JS 的事件循环4.JS 中的计时器能做到精确计时吗&#xff1f;为什么&#xff1f; 1.懒加载的原理及实现了解吗 原理&#xff1a;当图片没有到达可视范围内时&#xff0c;图片不加载&#xff0c;当图片一旦…

【Realtek sdk-3.4.14b】RTL8197FH-VG和RTL8812F自适应认证失败问题分析及修改

WiFi自适应认证介绍 WiFi 自适应可以理解为针对WiFi的产品,当有外部干扰信号通过,WiFi产品自动停止发出信号一段时间,以达到避让的目的。 问题描述 2.4G和5G WiFi自适应认证失败,信道停止发送信号时间过长,没有在规定时间内停止发包 2.4G截图 问题分析 根据实验室描述可以…

yolov7简化网络yaml配置文件

yolov7代码结构简单&#xff0c;效果还好&#xff0c;但是动辄超过70几个模块的配置文件对于想要对网络进行魔改的朋友还是不怎么友好的&#xff0c;使用最小的tiny也有77个模块 代码的整体结构简单&#xff0c;直接将ELAN结构化写成一个类就能像yolov5一样仅仅只有20几个模块&…

[H5动画制作系列] 奔跑的豹子的四种Demo演化

资源: bg.jpg: leopard.png: 背景透明 peopard2.png 背景不透明 参考代码1: leopard.js: (function(window) {ma = function() {this.initialize();}ma._SpriteSheet = new createjs.SpriteSheet({images: ["leopard.png"], frames: [[0,0,484,207],[486,0,484,20…

H5唤醒App

H5唤醒App 在App的业务场景中&#xff0c;H5唤醒App是一个几乎必备的功能。比如你想要实现以下需求&#xff1a;当App内容通过各种途径&#xff08;短信、二维码、微信等&#xff09;触达用户&#xff0c;从浏览器或者第三方内部可以直接打开H5网页&#xff0c;由网页端交互操作…

线性代数的本质(十)——矩阵分解

文章目录 矩阵分解LU分解QR分解特征值分解奇异值分解奇异值分解矩阵的基本子空间奇异值分解的性质矩阵的外积展开式 矩阵分解 矩阵的因式分解是把矩阵表示为多个矩阵的乘积&#xff0c;这种结构更便于理解和计算。 LU分解 设 A A A 是 m n m\times n mn 矩阵&#xff0c;…

web安全漏洞-SQL注入实验2

实验目的 学习sql显注的漏洞判断原理掌握sqlmap工具的使用分析SQL注入漏洞的成因 实验工具 sqlmapsqlmap是用python写的开源的测试框架&#xff0c;支持MySQL&#xff0c;Oracle&#xff0c;PostgreSQL&#xff0c;Microsoft SQL Server&#xff0c;Microsoft Access&#x…

SLS 1508 支持艾默生DeltaV报警抑制模块的设计

SLS 1508 支持艾默生DeltaV报警抑制模块的设计 宣布其SILAlarm V2.10合理化工具支持艾默生DeltaV报警抑制模块的设计和自动配置。这些模块通过在满足预定义条件时抑制来自操作员的警报来消除警报泛滥&#xff0c;并帮助用户符合ISA-18.2和IEC 62682标准。 报警抑制逻辑在艾默…

安全测试BurpSuite-抓包篡改数据

bp做安全测试很核心的一个步骤就是抓包 一、配置代理 浏览器代理&#xff0c;第一种使用浏览器自身手动代理 第二种下载插件&#xff08;推荐使用&#xff0c;切换多个代理方便&#xff09; 1、浏览器chrome/火狐安装插件 2、配置对应的代理情景 二、burp suite配置对应的…

List与ArrayList

目录 一、List及其使用 1.1 List的概念 1.2 常见接口的介绍 1.3 List的使用 二、线性表和顺序表 2.1 线性表 2.2 顺序表 三、ArrayList介绍 四、ArrayList的使用 4.1 ArrayList构造 4.2 ArrayList的常用方法 4.3 ArrayList的遍历 4.4 ArrayList的扩容机制 五、ArrayList的具…

LVS负载均衡群集NAT模式

LVS负载均衡群集NAT模式 一、集群与分布式1.1、集群的含义1.2、lvs模型1.3、系统性能扩展方式1.4、群集的三种类型1.4.1、负载均衡群集1.4.2、高可用群集1.4.3、高性能运算群集 1.5、LVS的负载调度算法1.5.1、轮询1.5.2、加权轮询1.5.3、最少连接1.5.4、加权最少连接1.5.5、ip_…

【Spring源码解析】一文读懂Spring注入模型:开发者必备知识

文章目录 什么是注入模型注入模型的种类案例分析例子一创建Bean对象创建配置类重写后置处理器创建测试类执行测试方法分析 例子二改写站点类&#xff0c;去掉Autowired注解重写后置处理器执行测试方法分析思考 ✨这里是第七人格的博客✨小七&#xff0c;欢迎您的到来~✨ &#…

华为云云耀云服务器L实例评测|怎么搭建企业综合Web平台

前言 记得2019年&#xff0c;公司搞混合云的时候&#xff0c;测试过多家公有云&#xff0c;其中就有华为云。因公司也在深圳&#xff0c;项目也比较急&#xff0c;我司业务上云经验又不足&#xff0c;华为官方获悉情况后&#xff0c;第二天就派了4人小团队到我司来交流&#x…