java多线程编程(二)一一>线程安全问题, 单例模式, 解决程线程安全问题的措施

引言: 

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


线程安全问题的原因:

一.操作系统的随机调度 :

二.多个线程修改同一个变量: 

三.修改操作不是原子的 :

四.内存可见性 :

五.指令重排序:  

解决上述的线程安全问题的措施:   



线程安全问题的原因:

 一.操作系统的随机调度 :  

1. 这是线程安全问题的 罪魁祸首 随机调度使⼀个程序在多线程环境下, 执行顺序存在很多的变数.

例子:这个代码返回结果就是随机调度的体现 

 现象: 

class MyThread extends Thread {
    @Override
    public void run() {

        while (true) {

            System.out.println("这⾥是t线程运⾏的代码");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        
        t.start();

        while (true) {
            System.out.println("这里是主线程");
            Thread.sleep(1000);
        }
    }
}

现象: 


二.多个线程修改同一个变量: 

上⾯的线程不安全的代码中, 涉及到多个线程针对 count 变量进行修改.
此时这个 count 是⼀个多个线程都能访问到的 "共享数据" 
 

例子:下面这个代码应该预期应该自增10w次,但是由于线程安全问题,达不到预期 
public class Demo11 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
            System.out.println("t1 结束");
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
            System.out.println("t2 结束");
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();

        // 一个线程自增 5w 次, 两个线程, 总共自增 10w 次. 预期结果, count = 10w
        System.out.println(count);
    }
}

三.修改操作不是原子的 : 

这里我们count++,时候站在操作系统层面,我们要进行大致三步:

load:把count的值读到寄存器里 

add: 把寄存器中的内容加1

save:  把寄存器写回内存  

进行以上以上操作时候由于操作系统随机调度多个线程之间,可能出现数据被覆盖的情况,这就是操作不原子的体现: 


四.内存可见性 : 

这个问题可以引入Java内存模型说明: 

线程之间的共享变量存在 主内存 (Main Memory).(主内存就是泛指的内存)
每⼀个线程都有自己的 "工作内存" (Working Memory) .(工作内存指的是CPU 的寄存器和高速缓存L1,L2,L3)
当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存(CPU 的寄存器), 再从工作内存(CPU 的寄存器),读取数据。 
其实是通过jvm和编译器来实现优化,把读内存优化为读寄存器了,有时候这个优化逻辑不符合我们的预期的逻辑出现细节上的偏差,就导致内存可见性问题

代码实例:一个线程读取一个线程修改
这个循环条件的flag判断是条件跳转指令cmp是寄存器操作会很快,while会循环很多次,jvm觉得每次觉得读到的都是0,直接就把 读内存优化为读寄存器了, 此时寄存器的值为0,此时用户输入 1 想结束线程时,t1线程读不到这个在内存中(主内存)的值,所以这个t1线程结束不了
public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while (flag == 0){

            }
            System.out.println("t1线程结束");
        });


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

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


五.指令重排序:

要说清楚这个问题就要引入一种设计模式:单例模式

 单例模式: 

单例模式能保证某个类在程序中只存在唯⼀⼀份实例, 而不会创建出多个实例,不能创建多个对象,这里有两种写法:饿汉模式和懒汉模式: 

1.饿汉模式:  类加载的同时, 创建实例:  
类加载时就new对象所以成为饿汉模式,注意构造方法私有化,防止类外多次实例化。
​
class Singleton {
 private static Singleton instance = new Singleton();//类加载时就new对象

//构造方法私有化,防止类外被实例化多个对象
 private Singleton() {

 }
 public static Singleton getInstance() {
 return instance;
 }
}

​

2.懒汉模式-单线程版:  

懒汉模式,能不实例化就不实例化所以的懒汉, 第⼀次使用的时候才创建实例

class Singleton {

 private static Singleton instance = null;
 private Singleton() {

 }

 public static Singleton getInstance() {
     if (instance == null) {
         instance = new Singleton();
     }
 
      return instance;
 }
}

 但是这个在多线程下还是存在线程安全问题的,而且还有一个指令重排序问题 。

 就算加锁(结合下面看看),解决了线程安全问题,但是instance = new Singleton();

new对象操作,细分为这三步:

1 .申请内存空间

2.构造对象(初始化)

3.内存空间首地址,赋值给引用变量  
由于指令重排序,可能会改变顺序,顺序可能从1,2,3一一>1,3,2  
在这个代码情况下就可能,在类外调用,getInstance方法拿到未初始化的对象导致线程安全问题。
 
class Singleton {
 private static Singleton instance = null;
 private static Object locker = new Object(); 
 private Singleton() {

 }
 public synchronized static Singleton getInstance() {
     
   if(instance == null) {//进一步优化效率,减少锁的阻塞状态,(instance == null才加锁才new对象)
     synchronized(locker){
      if (instance == null) {
         instance = new Singleton();
      }
    }

   }
     return instance;
 }

}



解决上述的线程安全问题的措施:   

操作系统的随机调度 是操作系统,计算机一脉传承,不能解决,
接下来我们围绕三~四~五~展开 

三.修改操作不是原子的 : 

这里我们可以把相关的操作打包起来,就是引入锁:

synchronized 关键字 - 监视器锁 monitor lock :

synchronized 会起到互斥效果, 某个线程执⾏到某个对象的 synchronized 中时, 其他线程如果也执行 到同⼀个对象 synchronized 就会阻塞等待.
进⼊ synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁 
 就和上厕所一样:

理解 "阻塞等待":
针对每⼀把锁, 操作系统内部都维护了⼀个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试 进行加锁, 就加不上了, 就会阻塞等待, ⼀直等到之前的线程解锁之后, 由操作系统唤醒⼀个新的线程, 再来获取到这个锁。
注意:这里还有一种应用程序级别的忙等,不涉及操作系统

三.的解决代码:

这里注意两个线程要加他一把锁,才有互斥的效果。 

 

 private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                for (int i = 0; i < 50000; i++) {
                    count++;
                }
            }

            System.out.println("t1 结束");
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker) {
                for (int i = 0; i < 50000; i++) {
                    count++;
                }
            }
            System.out.println("t2 结束");
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();

        // 一个线程自增 5w 次, 两个线程, 总共自增 10w 次. 预期结果, count = 10w
        System.out.println(count);
    }
  

四.内存可见性解决:  

这里引入volatile关键字:


1.volatile 能保证每次读取操作都是读内存

2.volatile 能保证变量的读取和修改不会出发指令重排序 

​public class{
public volitile 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(()->{
            Scanner in = new Scanner(System.in);
            System.out.println("请输入flag的值");
            flag = in.nextInt();
        });

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

​}

五.指令重排序:

饿汉模式是没有线程安全问题和指令重排序的,因为都是读操作

懒汉模式下就会有:

我们也加上volatile关键字:

​
class Singleton {
 private static volatile Singleton instance = null;//加上volatile 
 private static Object locker = new Object(); 
 private Singleton() {

 }
 public synchronized static Singleton getInstance() {
     
   if(instance == null) {//进一步优化效率,减少锁的阻塞状态,(instance == null才加锁才new对象)
     synchronized(locker){
      if (instance == null) {
         instance = new Singleton();
      }
    }

   }
     return instance;
 }

}

​

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

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

相关文章

ELK之路第四步——整合!打通任督二脉

ELK之路第四步——整合&#xff01;打通任督二脉 前言1.架构2.下载资源3.整合开始1.分别启动三个es2.启动kibana3.新建filebeat_logstash.yml配置文件4.修改logstash的启动配置文件5.启动logstash6.启动filebeat7.Kibana查看 4.结语 前言 在开始本篇之前&#xff0c;你需要用到…

【渗透测试】01-信息收集-名词概念

1、域名 什么是域名? 相当于网站的名字维基百科对域名的解释是&#xff1a;互联网上某一台计算机或计算机组的名称&#xff0c;用于在数据传输时标识计算机的电子方位&#xff08;有时也指地理位置&#xff09;。 网域名称系统&#xff08;Domain Name System&#xff09;有时…

支持ANC的头戴式蓝牙耳机,更有小金标认证,QCY H3 Pro体验

平时听音乐、看视频&#xff0c;大家都想获得更悦耳的音质体验&#xff0c;这时候蓝牙耳机就是性价比更高的一种方案&#xff0c;同时因其无线束缚、便携性高的特点&#xff0c;随时拿出来就能用。更不用说如今国产品牌的蓝牙耳机升级迭代速度非常快&#xff0c;百元的价位就可…

【Golang】Gin框架中跨域问题解决方案

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

Python飞舞蝙蝠

目录 系列文章 写在前面 完整代码 代码分析 写在后面 系列文章 序号直达链接爱心系列1Python制作一个无法拒绝的表白界面2Python满屏飘字表白代码3Python无限弹窗满屏表白代码4Python李峋同款可写字版跳动的爱心5Python流星雨代码6Python漂浮爱心代码7Python爱心光波代码…

【SSM-Day5】SpringMVC入门

【SSM-Day5】SpringMVC入门 Web->Servlet->Servlet容器MVC档案SpringMVC档案SpringMVC核心操作&#x1f4e2;建立连接RequestMapping&#xff1a;实现路由映射Controller/ResponseBody&#xff1a;表示某个类是否可以接收HTTP请求 &#x1f4e2;接收请求1. 直接接收一个请…

ChatGPT在解读历史股票收益预测中的外推和错误校准

论文地址&#xff1a;https://arxiv.org/pdf/2408.16707 原本地址&#xff1a;https://mp.weixin.qq.com/s/gL8ZTnpS0xJy7Qc73QEaGw 摘要 本文研究了大型语言模型&#xff08;LLM&#xff09;如何解释历史股票回报&#xff0c;并将其预测与众包股票排名平台的估计进行了比较。…

binlog 介绍

公司使用的数据库是 glodendb https://www.zte.com.cn/china/solutions_latest/goldendb.html#idpart01 最近自动化投产对一张 28w 的表进行全表 update、投产前已经是知道这个数据量的、认为没有问题、也没有在测试环境进行测试。 结果投产那天直接报错 这个 transaction_max…

什么是x86架构,什么是arm架构

什么是 x86 架构&#xff1f; x86 架构是一种经典的指令集架构&#xff08;ISA&#xff09;&#xff0c;最早由英特尔在 1978 年推出&#xff0c;主要用于 PC、服务器等领域。 它是一种复杂指令集计算&#xff08;CISC&#xff09;架构&#xff0c;支持大量的复杂指令和操作&…

18.04Ubuntu网络一直connecting的问题

有段时间没登VMware的Ubuntu了&#xff0c;就知道这个Ubuntu一登必有问题。 如果你的网络一直connecting 设置成桥接模式就可以了&#xff01;

linux离线安装Ollama并完成大模型配置(无网络)

这篇文章主要分享两方面内容&#xff1a; 1&#xff09;在纯内网环境下如何部署ollama 2&#xff09;在纯内网环境下如何配置大模型 话不多说直接开始。 ①离线部署ollama 一、通过浏览器访问ollama官方安装脚本&#xff0c;获取脚本内容。复制里面的内容。 在Linux中执行…

Linux笔记---Makefile的简单用法

1. 什么是Makefile Makefile 是一种用于自动化构建和管理项目的工具&#xff0c;特别是在软件开发中非常常见。 它包含了一系列规则&#xff08;rules&#xff09;和指令&#xff0c;描述了如何编译和链接源代码文件&#xff0c;以及生成最终的可执行文件或库文件。 简单来说&a…

stm32入门教程--USART外设 超详细!!!

目录 简介 什么是UART&#xff1f; 什么是USART&#xff1f; 简介 USART&#xff08;Universal Synchron /Asynchronous Receiver /Transmitter&#xff09;通用同步/异步收发器 1、USART是STM32内部集成的硬件外设&#xff0c;可根据数据寄存器的一个字节数据自动生成数据帧…

启动任务节点时,传入机械臂参数

MoveIt2_tutorial-Examples-Planning Scene ROS API 先启动move_group节点&#xff0c;后启动任务节点 方式1&#xff1a; 直接传入参数文件的地址 from launch import LaunchDescription # 导入启动描述&#xff0c;用于描述启动文件的结构 from launch_ros.actions impor…

规划误差降低27%,碰撞率降低33%Senna: 大规模视觉-语言模型与端到端自动驾驶相结合

Abstract 端到端自动驾驶在大规模数据中展示了强大的规划能力&#xff0c;但在复杂、罕见的场景中仍然因常识有限而表现不佳。相比之下&#xff0c;大型视觉语言模型&#xff08;LVLMs&#xff09;在场景理解和推理方面表现出色。前进的方向在于融合两者的优势。以往利用LVLMs…

Charles简单压力测试

1.接口请求次数&#xff0c;并发量&#xff0c;请求延迟时间均可配置 1.1选中需要进行测试的接口&#xff0c;鼠标右键选中【repeat advance】 2.设置并发参数 下面的图中&#xff0c;选择了1个接口&#xff0c;每次迭代中1个接口同时请求&#xff0c;迭代1000次&#xff08;…

Zookeeper 对于 Kafka 的作用是什么?

大家好&#xff0c;我是锋哥。今天分享关于【Zookeeper 对于 Kafka 的作用是什么&#xff1f;】面试题&#xff1f;希望对大家有帮助&#xff1b; Zookeeper 对于 Kafka 的作用是什么&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 ZooKeeper 在 Kafka…

【机器学习基础】激活函数

激活函数 1. Sigmoid函数2. Tanh&#xff08;双曲正切&#xff09;函数3. ReLU函数4. Leaky ReLU函数 1. Sigmoid函数 观察导数图像在我们深度学习里面&#xff0c;导数是为了求参数W和B&#xff0c;W和B是在我们模型model确定之后&#xff0c;找出一组最优的W和B&#xff0c;使…

leetcode-62-不同路径

题解&#xff1a; 1、设dp[i][j]代表到达(i,j)点最多的路径&#xff1b;题目要求机器人每次只能向右或向下走一步&#xff0c;所以到达(i,j)点的最多路径为到达(i-1,j)的最多路径与到达(i,j-1)的最多路径之和。即dp[i][j]dp[i-1][j]dp[i][j-1]。 2、初始化一个M*N的矩阵dp,将…

C++在实际项目中的应用第三节:C++与数据科学

第五章&#xff1a;C在实际项目中的应用 第三节&#xff1a;C与数据科学 1. C在数据分析中的实际应用 数据分析是数据科学的核心部分&#xff0c;主要涉及数据的清洗、转换和建模。C作为一种高性能的编程语言&#xff0c;越来越多地被应用于数据分析领域。以下是 C 在数据分…