Java 多线程(四)—— 线程安全 与 volatile 与 单例模式

什么是线程安全

在进行多线程编程的时候,当我们编写出来的多线程的代码运行结果不符合我们的预期的时候,这时候就是 bug,这种 bug 是由于多线程的问题而产生出来的 bug 我们称之为 线程安全问题

当我们编写出来的多线程代码运行之后的结果符合我们的预期结果的时候,说明代码没有问题,这时候就是 线程安全

线程安全问题的产生与解决方案

线程安全问题的产生主要有 五个原因

线程的调度是随机的

这个原因是由操作系统产生的,CPU 是多核心的,在进行线程的调度的时候并不是等到线程彻底执行完才轮到下一个线程执行,CPU 使用的是抢占式执行,也就是说,这个线程可能执行到一半,就立马被剥夺了 CPU 资源,开始执行下一个线程,然后执行完一半,又将上一个线程调度回来,这是由随机性的,程序员无法通过代码应用层得知。

这个问题是无法改变的,这也就是为什么会产生线程安全问题的最根本的原因

多个线程对同一个变量进行修改

在之前的文章中就已经设计过这种情况的讨论,如果修改的外部类的成员变量,是会发生线程安全问题的,如果修改的是局部变量,那就会触发 “变量捕获的语法”,这时候是不建议进行修改的。

解决方法也很简单,就是加锁,通过 synchronized 进行加锁。

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

线程的修改操作不是原子性的

这个问题其实和第二个问题是一样的,为什么修改同一个变量可能会发生线程安全问题,因为我们的修改指令并不是原子性的,也就说,这个操作并不是 CPU 执行一次指令就可以完成 count++ 的,count ++ 实质是由三条指令实现的,首先 load count 这个数值,然后进行 count +1 操作,最后将结果保存到内存里。

为了使修改操作是原子性的,所以我们使用加锁的方式来实现,也就是上面的代码。

内存可见性问题

这个问题是由于 JVM 优化而导致的,

public class Test {private static int flag = 1;public static void main(String[] args) {Thread t1 = new Thread(() -> {while(flag == 1) {}System.out.println("hello t1");});Thread t2 = new Thread(() -> {Scanner scan = new Scanner(System.in);System.out.println("请输入falg 的数值");flag = scan.nextInt();});t1.start();t2.start();}
}

在这里插入图片描述

即使我们修改了 flag 数值,但是程序依旧没有反应,说明在 t1 线程中读取到的 flag 依旧还是 1

一个线程涉及到了读操作,一个线程涉及到了修改操作,这可能会触发线程安全问题,也就是内存可见性问题,读操作没有读到修改过的数值。

原因:JVM / 编译器 其实是带有优化功能的,因为不同的程序员写出来的代码不同,运行效率也是不同,为了提高代码的运行效率,JVM / 编译器 在不改变我们代码的逻辑的情况下,会对我们写的代码进行优化。虽然说对我们代码逻辑不会做出改变,但是在多线程编程下可能会发生误判。

例如上面的代码,t1 线程进行读 flag 操作,也就是寄存器会从内存中读取 flag ,但是这是一个 while 循环,在一秒钟之内就会读取很多次,虽然 t2 线程会对 flag 进行修改,但是 t2 线程在启动之前 flag 这个数值就被 t1 线程读取了 几千万次,所以编译器 / JVM 会认为 flag 是一个不会被修改的数值,即把这个读内存操作优化为 读寄存器操作,也就是把 flag 这个数值拷贝一份到寄存器里,这样 CPU 就直接从寄存器读 flag 数值而不用到 内存中读取了。

等到了 t2 线程开始运行的时候,我们进行修改 flag 数值,内存中 flag 即使被修改了,但是 t1 线程还是不知道flag 被修改了,因为此时它是从寄存器读取 flag 数值。


拓展一下,如果我们在 t1 线程 加上 sleep 的话,这个内存可见性问题就消失了。

    private static int flag = 1;public static void main(String[] args) {Thread t1 = new Thread(() -> {while(flag == 1) {try {Thread.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("hello t1");});Thread t2 = new Thread(() -> {Scanner scan = new Scanner(System.in);System.out.println("请输入falg 的数值");flag = scan.nextInt();});t1.start();t2.start();}

在这里插入图片描述

即使是 sleep 1 ms 内存可见性问题也没有发生,这是为什么?

因为读内存操作可能就是几 ns 的事情,优化为 读寄存器操作可以再快个几 ns,但是代码存在 sleep 1 ms ,这个 1ms 的存在,编译器/ JVM 即使优化这个读操作也不能让代码的效率有一个质的飞跃,所以干脆就不提升了。所以内存可见性问题也就不存在了。

JVM / 编译器的优化是一个很复杂的事情,具体的细节大家可以参考深入理解Java虚拟机 这本书,在后续文章中也会提到 JVM 的部分内容。


如何解决这个内存可见性问题???
使用 volatile 关键字

在这里插入图片描述

这个关键字的英文翻译的易变的,说明这个变量我是会进行修改的,你不能进行读操作的优化。

注意这个关键字只能修饰变量,不能修饰方法!!!

修改后的代码:

import java.util.Scanner;public class Test {private static volatile int flag = 1;public static void main(String[] args) {Thread t1 = new Thread(() -> {while(flag == 1) {}System.out.println("hello t1");});Thread t2 = new Thread(() -> {Scanner scan = new Scanner(System.in);System.out.println("请输入falg 的数值");flag = scan.nextInt();});t1.start();t2.start();}
}

在这里插入图片描述


指令重排序问题

这个问题在下面的单例模式中的懒汉模式会提到~~

单例模式

单例模式是一种设计模式,也就是一个规范。

单例模式,顾名思义就是只允许一个对象的创建,也就是一个类只能创建实例化一个对象,不能进行多次实例化。这种设计模式的应用场景还是很多的,例如:我们在进行服务器开发的时候,我们需要一个对象来存放数据,这时候我们就会先写出类,然后再去创建对象,但是如果这个对象包含的数据很大,假如有100G,那么创建多次之后,也就是有几百G 的数据需要放在服务器上,并且这么多重复的数据也就只有一份是有用的,不仅仅是浪费了服务器的内存资源,还可能会导致服务器的崩溃,在这种情况下,我们通常使用单例模式来进行约束,只允许一个对象的创建。

饿汉模式

饿汉模式 是程序已启动,随着类的加载,对象也随之创建出来了,所以称之为 饿汉模式,说明创建的很快。

class Singleton {private static Singleton instance = new Singleton();private Singleton() {}public Singleton getInstance() {return instance;}
}

从上面的代码,我们就可以看到是要类一加载,对象instance 也就创建出来了private static Singleton instance = new Singleton();

为什么说我们不能进行多次创建呢?
因为这个类的构造方法被我们用private 修饰了,在外面是不能进行实例化的,这也是单例模式的点睛之笔。

我们来讨论一下,这个饿汉模式 的代码会不会出现线程安全问题?
答案是不会的,线程只是从getInstance() 进行读操作,获取 instance 这个对象,并没有涉及到修改操作,自然没有线程安全问题的存在。

懒汉模式

懒汉模式 顾名思义就是 懒,等我们真正需要这个对象的时候,才会进行实例化对象的操作。我们来看一下代码:

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

当我们真正需要用到这个对象的时候,才进行实例化,这就是懒汉模式。

但是在多线程编程下,是可能会出现线程安全问题,由于代码涉及到写操作,也就是 实例化对象的操作,假设有两个线程同时进行对象的实例化,就会发生线程安全问题,所以要加上锁 synchronized .

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

但是每次进行判断的时候都需要进行加锁,这就导致效率低下,所以我们在外面再加一层 if 判断,减少加锁的次数。

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

即使代码被我们修改成这样,还是会存在一个问题,指令重排序的问题

我们在实例化一个对象有三条指令需要做:第一申请内存空间,第二初始化对象,第三将内存空间的首地址赋值给引用。

在编译器/JVM 下可能会进行优化,将上面的三条指令优化为先执行1,再执行3 ,最后执行 2.

这可能会导致一个线程还没初始化对象,另一个线程就直接拿到这个对象进行使用了,但是这些使用操作,在后面的初始化完之后又被覆盖掉了。这就是第五个引起线程安全问题的原因 —— 指令重排序。

在这里插入图片描述

如何解决这个问题???
使用 volatile 关键字

没错 volatile 关键字不仅仅能解决内存可见性问题,还能解决指令重排序问题。

private static volatile SingletonLazy instance;

懒汉模式最终代码

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

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

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

相关文章

计算机毕业设计选题推荐【基础功能+创新点】【Java方向】

选以下是30个Java方向的精选毕设选题&#xff0c;每个选题包括基础功能和创新点&#xff0c;供你参考。 文末有作者名片喔&#xff0c;需要交流 (毕业) 可联系 在线教育平台 基础功能&#xff1a;用户注册与登录&#xff0c;课程浏览与搜索&#xff0c;课程购买与观看&#xf…

学习文档(5)

Redis应用 目录 Redis应用 Redis 除了做缓存&#xff0c;还能做什么&#xff1f; Redis 可以做消息队列么&#xff1f; Redis 可以做搜索引擎么&#xff1f; 如何基于 Redis 实现延时任务&#xff1f; Redis 除了做缓存&#xff0c;还能做什么&#xff1f; 分布式锁&…

WPF中的常见控件

控件模板ControlTemplate 在 WPF (Windows Presentation Foundation) 中&#xff0c;ControlTemplate 是一个 XAML 模板&#xff0c;它定义了控件的视觉结构和布局。通过使用 ControlTemplate&#xff0c;你可以自定义控件的外观&#xff0c;包括它的样式、颜色、形状以及包含…

三周精通FastAPI:5 查询参数和字符串校验

FastAPI手册&#xff1a;https://fastapi.tiangolo.com/zh/tutorial/query-params-str-validations/ 查询参数和字符串校验 FastAPI 允许你为参数声明额外的信息和校验。让我们以下面的应用程序为例&#xff1a; from fastapi import FastAPIapp FastAPI()app.get("/it…

基于springboot+thymeleaf+springsecurity搭建一套web小案例

一、前言 本案例中的源代码已上传到资源库&#xff0c;可自行下载&#xff0c;传送阵 https://download.csdn.net/download/qq_36260963/89906196 Spring Boot是为了简化Spring应用的创建、运行、调试、部署等而出现的&#xff0c;使用它可以做到专注于Spring应用的开发&#x…

git clone 鉴权失败

git clone 鉴权失败问题 1. 问题描述2. 解决方法 1. 问题描述 使用git clone自己的代码报如下错误&#xff1a; 正克隆到 xxx... Username for https://github.com: Password for https://xxxgithub.com: remote: Support for password authentication was removed on Augu…

RAG流程的实现与改进

一、 RAG流程图 数据入库&#xff1a;读取本地数据并切成小块&#xff0c;并把这些小块经过编码embedding后&#xff0c;存储在一个向量数据库中&#xff08;下图1——6步&#xff09;&#xff1b;相关性检索&#xff1a;用户提出问题&#xff0c;问题经过编码&#xff0c;再在…

Vue项目中实现拖拽上传附件:原生JS与Element UI组件方法对比

在现代化的Web应用中&#xff0c;文件上传是一个基本功能。随着技术的发展&#xff0c;拖拽上传已经成为提升用户体验的一个重要特性。在Vue项目中&#xff0c;我们可以通过原生JavaScript或使用Element UI组件来实现这一功能。下面我们将分别介绍这两种方法&#xff0c;并对比…

吴恩达深度学习笔记(6)

正交化 为了提高算法准确率&#xff0c;我们想到的方法 收集更多的训练数据增强样本多样性使用梯度下降将算法使算法训练时间更长换一种优化算法更复杂或者更简单的神经网络利用dropout 或者L2正则化改变网络框架更换激活函数改变隐藏单元个数 为了使有监督机制的学习系统良…

vue使用jquery的ajax,页面跳转

一、引入jquery依赖 打开终端更新npm npm install -g npm 更新完后引入输入npm install jquery 加载完后 在最外层的package.json文件中加入以下代码 配置好后导入jquery 设置变量用于接收服务器传输的数据 定义ajax申请数据 服务器的Controller层传输数据 &#xff08;…

【VUE小型网站开发】初始环境搭建

1. 初始化VUE项目 1.1 创建vue项目 1.2 删除多余的界面 根据自己情况删除红框内的文件 清理app页面代码 1.3 引入vue-router 1.3.1 下载vue-router npm install vue-router1.3.2 配置vue-router 在 main.js 或 main.ts 中引入 vue-router import ./assets/main.css im…

Android 图片相识度比较(pHash)

概述 在 Android 中&#xff0c;要比对两张 Bitmap 图片的相似度&#xff0c;常见的方法有基于像素差异、直方图比较、或者使用一些更高级的算法如 SSIM&#xff08;结构相似性&#xff09;和感知哈希&#xff08;pHash&#xff09;。 1. 基于像素的差异比较 可以逐像素比较…

【SQL】写SQL查询时,常用到的日期函数

我们在做SQL的查询&#xff0c;日期函数是我们经常会用得到的函数&#xff0c;可以方便调用用于处理日期和时间数据。 以下是一些常见的日期函数及其用法示例&#xff1a; 1. 直接报出日期和时间 CURRENT_DATE&#xff1a;返回当前日期。NOW()&#xff1a;返回当前日期和时间…

【tensorrt install 】

无需点击download的下载界面&#xff0c;直接可选TensorRT Download | NVIDIA Developer 下载的是 tar type &#xff08;推荐&#xff09;&#xff1a;- TensorRT 10.5 GA for Linux x86_64 and CUDA 12.0 to 12.6 TAR Package 下载的是deb type&#xff1a; TensorRT 10.5 GA…

基于MATLAB车牌识别系统设计

MATLAB车牌识别系统设计 实践目的 车牌是一辆汽车独一无二的信息&#xff0c;因此&#xff0c;对车辆牌照的识别技术可以作为 辨识一辆车最为有效的方法。随着ITS(智能交通系统)的高速发展&#xff0c;对车牌识别技术的研究也随之发展。从根本上讲&#xff0c;牌照识别应用了…

中缀表达式转后缀表达式(逆波兰表达式)及如何计算后缀表达式

目录 中缀、后缀表达式简介 中缀转后缀的规则 模拟中缀转后缀 中缀转后缀代码 后缀表达式求值 后缀表达式求值代码 Leetcode相关题目 中缀、后缀表达式简介 首先说说什么是中缀表达式&#xff0c;中缀表达式中&#xff0c;操作符是以中缀形式处于操作数的中间。例如&…

ImportError: DLL load failed while importing _rust: 找不到指定的模块。

ImportError: DLL load failed while importing _rust: 找不到指定的模块。 安装qwen-agent 过程需要安装 cryptography 报错 &#xff1a; 177 WARNING: Failed to collect submodules for ‘cryptography.hazmat.backends.openssl’ because importing ‘cryptography.hazm…

【openwrt-21.02】T750 openwrt 概率出现nat46_ipv4_input+0x90/0x4b4问题分析及解决方案

Openwrt版本 NAME="OpenWrt" VERSION="21.02-SNAPSHOT" ID="openwrt" ID_LIKE="lede openwrt" PRETTY_NAME="OpenWrt 21.02-SNAPSHOT" VERSION_ID="21.02-snapshot" HOME_URL="https://openwrt.org/" …

Linux安装Anaconda和Pytorch

又到了一年一度换环境、换服务器不断折腾的时节了&#xff0c;一通折腾后&#xff0c;重新启动遂做记录。 1. Linux安装Anaconda 1.1 离线安装模式 进入官网https://www.anaconda.com/download/success&#xff0c;如图所示&#xff1a; 选择版本进行下载即可。 1.2 在线w…

基于IP的真实地址生成器

ip-geoaddress-generator 是一个基于 Web 的在线应用程序&#xff0c;能够根据 IP 地址生成真实的随机地址信息。通过多个 API 获取位置数据和随机用户信息&#xff0c;该工具为用户提供了完整的虚拟身份。它由 Next.js 和 Radix UI 构建&#xff0c;具备自动检测当前 IP 地址和…