02、并发编程的三大特性

并发编程有三大特性分别是,原子性,可见性,有序性。会产生这些特性的根本原因是现在的服务器都是多CPU多核心数的,每个CPU都有自己单独的一套缓存和pc系统,而且程序在运行时按照JMM的规范,它们是需要先把数据从主内存中读取到工作内存(也就是运行这个线程的CPU的缓存中),对工作内存中的数据进行修改之后再写回主内存,在对自己工作内存数据的操作对其他CPU是不可见的,这才会导致并发编程会有以上三种特性。下面就三种特性的概念,产生的问题,和解决方案进行阐述

image.png

1、原子性

1.1、原子性的定义

在并发编程中,会出现多个线程同时修改同一个变量的情况,此时对应的临界区域(多个线程都运行的代码区域)不做任何的限制的话,就会出现,a线程把i变量由1修改成2了,还没有来得及给i赋值2的时候,b线程也对i进行操作,但是它拿到的是原来的1,这样a,b两个线程都+1可结果还是2,为了解决这个问题,就需要保证a,b线程在对i修改之后把i写回主内存这一系列的操作,没有其他线程进行干扰,一次性全部完成之后,然后其他的线程再对i操作,由此就引出了并发编程中的原子性的定义

  • 原子性:保证多线程运行过程中,临界区域的代码在运行的时候,是不可分割不可被打断的,其间也不会有其他的线程对其进行干扰。

1.2、原子性的解决方案

1.2.1、通过synchronizd锁的方案
    private static int count;
    //通过synchronized实现
    public static synchronized void increment(){
        count++;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                increment();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

通过查看上面带synchronized底层编译的JVM操作指令可以发现,synchronized在JVM指令操作上在临界区域加上了monitorenter操作,直到临界区域完成才会有monitorexit(异常也会有一个monitorexit操作),退出操作

image.png

1.2.2、通过lock锁的方案
    private static int count;
    private static ReentrantLock lock = new ReentrantLock();
    public static void increment()  {
        //通过lock的方式实现
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }


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

ReentrantLock的底层是基于AQS实现的,它是通过CAS的方式维护一个state变量来实现锁的操作

  • CAS(compare and swap)
    CAS是一条CPU层级就支持的原子操作,根据CAS的定义:比较和交换,底层的实现原理是,当它要置换内存中某个位置的值时,它会先去比较下是否和预期的值一致,如果一致它才置换。
  • CAS的缺点
    它只能保证对一个变量的操作是原子性的,不能实现对多行代码实现原子性。
  • CAS的问题
    • ABA问题:一个变量一开始是A,经过修改之后由A变成B,之后又变成A,此时对于那些引用类型是有问题的,因为引用类型虽然变回了A,但是它引用的指向的地址里的具体内容很可能已经发生了变化。
    • ABA问题的解决方案:在比较的时候不仅比较预期的值,还需要比较对应的版本号,这样在由A->B,又B->A的过程中,版本号肯定跟原先的不一致,由此可以判断出已经修改不能进行置换操作
1.2.3、通过ThreadLocal的方案
static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();

public static void main(String[] args) {
    tl1.set("123");
    tl2.set("456");
    Thread t1 = new Thread(() -> {
        System.out.println("t1:" + tl1.get());
        System.out.println("t1:" + tl2.get());
    });
    t1.start();

    System.out.println("main:" + tl1.get());
    System.out.println("main:" + tl2.get());
}

ThreadLocal解决线程之间的原子方案是通过线程隔离实现的,直接把共享变量分配给每个线程,每个线程只能操作自己的这个变量,把共享变量变成线程私有的变量

  • ThreadLocal的底层实现
    1、每个线程都有一个ThreadLocalMap对象作为Thread的成员变量
    2、调用ThreadLocal的set方法时会初始化对应Thread的ThreadLocalMap变量
    3、把当前的ThreadLocal对象作为key,把对应要存的数据作为value存储
    image.png
  • ThreadLocal会产生内存泄露问题

由于一般情况在创建ThreadLocal的时候都会把它设置成Static,所以就算是a线程运行结束了,a线程的对应的ThreadLocal对象由于Static指向着所有不会释放内存,但是由于线程运行结束,线程产生的独有资源,ThreadLocalMap中的key和value应该释放掉,如果不释放新的线程不断产生,会不断的消耗系统内存,从而导致内存泄露

  • 解决方案

上述ThreadLocalMap的key和value内存泄露问题,其中key的内存泄露,系统已经帮我们做好了

  • 系统通过把key设置成WeakReference类型,能做到当这个线程运行完成,GC回收的时候就会把ThreadLocal对象回收。因为弱引用的特点是只要碰到GC回收,它就会被回收。
  • 至于value的回收,需要我们在代码里手动的调用ThreadLocal中的remove方法把value释放掉

2、可见性

2.1、可见性定义

导致可见性问题的原因是多CPU独立运行时,只会修改各自的工作内存也就是CPU缓存数据,而且多个CPU之间是有独立的缓存和PC系统。

  • 可见性:多线程在运行的时候一个线程修改了公共变量,其他的线程不能及时的见到

2.2、可见性的解决方案

2.2.1、volatile方案
    //通过volatile修饰,控制t1线程的停止
    private volatile static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (flag) {
                // ....
            }
            System.out.println("t1线程结束");
        });

        t1.start();
        Thread.sleep(10);
        flag = false;
        System.out.println("主线程将flag改为false");
    }

上述代码中如果没有volatile修饰,flag在主线程里被修改成false,t1线程是不会感知到的,那volatile底层是怎么实现的呢?

  • volatile实现细节
    • volatile在JVM层面是通过带lock前缀的指令实现。
    • JVM的指令在CPU层面是通过缓存一致性协议,如MESI协议(inter的协议)实现。
    • volatile变量在写操作时,JVM会及时的把对应CPU的缓存行刷到主内存中,在volatile变量被修改之后,根据MESI协议,所有CPU中对应这行缓存行数据都会失效,得重新去主内存中读取。
2.2.2、synchronized或是lock方案

锁的方案是可以实现多线程间变量的可见性的。锁的定义就是让单个线程去修改共享变量,修改之后其他线程再去读取,这个时候其他线程读到的数据肯定是上个线程修改的最新数据

2.2.3、final方案

final修饰的变量初始化之后,就不能被修改,所有的线程拿到的都是同一个值,也是间接的实现了线程之间的可见性

3、有序性

3.1、有序性定义

由于CPU的运行速度和读取数据的速度相差好几个数量级的关系,所以现代CPU为了追求效率,会进行“乱序执行”,在运行到需要去内存中读数据的指令时,可能要花很长时间等待读取数据,在这些时间的等待中,在保证程序结果的最终一致性之后,CPU就进行指令重排,以提高效率,但是有些时候这中乱序是不被允许的。

  • 有序性:让程序中的指令集按照顺序执行,不让CPU进行指令重排

3.2、有序性解决方案

3.2.1、volatile方案
//通过volatile关键词解决
private static volatile MiTest test;
private MiTest(){}
public static MiTest getInstance(){
    // B
    if(test  == null){
        synchronized (MiTest.class){

            if(test == null){
                // A   ,  开辟空间,test指向地址,初始化
                test = new MiTest();
            }
        }
    }
    return test;
}

上述是一个单例模式的样例代码,如果不加volatile关键词,在new MiTest这个共享对象的时候,就会出现问题,对象的new过程在底层一共有三个操作指令,分别是,给对象开辟空间,给对象初始化,把地址引用赋值给变量。如果此时发生了指令的重排,顺序变成了,给对象开辟空间,把地址引用赋值给变量,给对象初始化,那么B线程此时就会拿到没有进行初始化好的对象使用,就会发生问题,通过volatile防止指令重排就可以解决这个问题。

  • volatile防止指令重排的细节
    它是通过内存屏障来实现的,内存屏障相当于是一条指令,在这条指令的前后操作指令不能重排,在JVM层面有对应的读写屏障,在CPU底层也有对应的读写屏障来具体支持

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

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

相关文章

基于Java+Jsp Servlet Mysql实现的Java Web在线商城项目系统设计与实现

一、前言介绍&#xff1a; 1.1 项目摘要 随着互联网技术的飞速发展&#xff0c;电子商务已成为现代商业活动的重要组成部分。在线商城作为电子商务的一种重要形式&#xff0c;以其便捷性、高效性和广泛覆盖性&#xff0c;受到了越来越多消费者的青睐。同时&#xff0c;随着消…

【安全测试相关知识】

安全测试介绍 背景 在当前信息技术快速发展的背景下&#xff0c;网络安全问题日益严峻&#xff0c;数据泄露、黑客攻击、病毒传播等安全事件层出不穷&#xff0c;给个人、企业乃至国家带来严重威胁。所以安全测试已成为企业和国家关注的重心 作用 安全测试是确保软件系统安…

WPS如何快速将数字金额批量转换成中文大写金额,其实非常简单

大家好&#xff0c;我是小鱼。 在日常的工作中经常会遇到需要使用金额大写的情况&#xff0c;比如说签订业务合同时一般都会标注大写金额&#xff0c;这样是为了安全和防止串改。但是很多人也许不太熟悉金额大写的方法和习惯&#xff0c;其它没有关系&#xff0c;我们在用WPS制…

Element-ui的使用教程 基于HBuilder X

文章目录 1.Element-ui简介2.使用HBuilderX 创建一个基于Vue3的项目 &#xff08;由于是使用的基于Vue3的Element-ui&#xff09;3.安装element-ui4.在项目里完全引用element-ui5.引用组件6.运行项目 1.Element-ui简介 Element&#xff0c;一套为开发者、设计师和产品经理准备…

MySQL的架构设计和设计模式

1. 数据库设计模式与范式 数据库设计模式是解决数据库设计中常见问题的一种思维方式&#xff0c;它提供了一套解决方案。以下是一些常见的数据库设计模式和范式&#xff1a; 实体-关系模型&#xff08;Entity-Relationship Model&#xff09;&#xff1a;通过实体和实体之间的…

【MySQL】十三,关于MySQL的全文索引

MySQL的全文索引用于搜索文本中的关键字&#xff0c;类似于like查询。 演示 建表 CREATE TABLE demo (id INT(11) NOT NULL,name CHAR(30) NOT NULL,age INT(11) NOT NULL,info VARCHAR(255),primary key(id),fulltext index futxt_idx_info(info) );此表的默认存储引擎为In…

Aloudata 入选 IDC「GenAI+Data」中国市场代表厂商

近期&#xff0c;国际知名技术研究与咨询机构 IDC 发布了《GenAIData 市场趋势分析及最佳实践案例》报告&#xff0c;总结了当前主要市场特点和数据变化影响&#xff0c;并给出技术布局建议&#xff0c;以供市场参考。报告中还绘制了 GenAIData 发展趋势图&#xff0c;从市场需…

NCR+可变电荷块3——NCB/cell绘图1

文献method参考&#xff1a; 蛋白质序列数据从uniprot中获取 https://www.uniprot.org/uniprotkb/P46013/entry https://www.uniprot.org/uniprotkb/P06748/entry、 1&#xff0c;电荷分布计算&#xff1a; Charge distribution was calculated as the sum of the charges …

单片机锂电池电量电压检测

一、引言 &#xff08;一&#xff09;锂电池电量检测的重要性简述 在如今这个科技飞速发展的时代&#xff0c;众多电子设备都依赖锂电池来供电&#xff0c;像我们日常使用的智能手机、平板电脑、笔记本电脑&#xff0c;还有出行必备的电动自行车、电动汽车等等&#xff0c;锂…

支付宝订单码支付

1.订单码支付&#xff0c;首先下载官方网站提供的sdk包到你的项目中。 2.选择控制器复制官方文档的获取二维码相关的代码示例。打开sdk包中v2的index.php文件&#xff0c;这个才是你选择语言的具体代码。 3.引用里面所需要的类文件&#xff0c;文件下载到你的项目中后&#xf…

【HarmonyOS 5.0】第十二篇-ArkUI公共属性(一)

一、公共样式类属性 ArkUI框架提供的基础组件直接或者间接的继承自 CommonMethod &#xff0c; CommonMethod 中定义的属性样式属于公共样式。下面就来学习这些样式 1.1.尺寸设置 宽高设置 设置组件的宽高&#xff0c;缺省时使用组件自身内容的宽高&#xff0c;比如充满父布…

VTK知识学习(27)- 图像基本操作(二)

1、图像类型转换 1&#xff09;vtkImageCast 图像数据类型转换在数字图像处理中会频繁用到。一些常用的图像算子(例如梯度算子)在计算时出于精度的考虑&#xff0c;会将结果存储为float或double类型&#xff0c;但在图像显示时&#xff0c;一般要求图像为 unsigned char 类型,…

Go C编程 第6课 无人机 --- 计算旋转角

旋转的秘密---认识角度 rt、lt命令学习 goc电子课程 一、编程步骤 第一步 第二步 第三步 第四步 二、画“四轴无人机” &#xff08;一&#xff09;、画第一根机轴 &#xff08;二&#xff09;、画第二根机轴 &#xff08;三&#xff09;、画完整的无人机 三、画“多轴无人…

cursor保存更改操作技巧

1. 当我们在agent模式时&#xff0c;要求cursor更改代码时&#xff0c;cursor回答后&#xff0c;就已经更改了代码了&#xff0c;这时候就可以对程序进行编译和测试&#xff0c; 不一定先要点” accept“, 先测试如果没有问题再点“accept”&#xff0c;这样composer就会多一条…

graphRAG+llama3.2的MOOC课程资源问答系统

文章目录 参考代码地址anacondapycharmLLaMA 3传统ragGraphRAG初始化提示词微调 prompt tuning来创建更适应知识库的知识图谱使用语言模型&#xff08;LLM&#xff09;从每个文本块中提取实体、关系和声明。检索 query&#xff08;本地搜索&#xff08;Local Search&#xff09…

一键打断线(根据相交点打断)——CAD c# 二次开发

多条相交线根据交点一键打断&#xff0c;如下图&#xff1a; 部分代码如下: finally namespace IFoxDemo; public class Class1 {[CommandMethod("ddx")]public static void Demo(){//"ifox可以了".Print();Database db HostApplicationServices.Workin…

Websocket客户端从Openai Realtime api Sever只收到部分数据问题分析

目录 背景 分析 解决方案 背景 正常情况下&#xff0c;会从Openai Realtime api Sever收到正常的json数据,但是当返回音频数据时&#xff0c;总会返回非json数据。这是什么问题呢&#xff1f; 分析 期望的完整响应数据如下&#xff1a; {"session": {"inp…

flask后端开发(1):第一个Flask项目

目录 一、Helloworddebug、host、port的配置 一、Helloword 一般是会创建两个文件夹和app.py app.py from flask import FlaskappFlask(__name__)app.route(/) def hello_world():return Hello World!if __name__ __main__:app.run()右键运行这个py文件&#xff0c;消息绑定…

OAuth 2.0

简介 OAuth 是一种开放标准的授权协议或框架&#xff0c;它提供了一种安全的方式&#xff0c;使第三方应用程序能够访问用户在其他服务上的受保护资源&#xff0c;而无需共享用户的凭证&#xff08;如用户名和密码&#xff09;。OAuth 的核心思想是通过“授权令牌”来代替直接…

玩原神学编程-原神时钟

前言 最近喜欢玩原神这种开放世界探索的游戏&#xff08;还有黑神话、古墓丽影等&#xff09;&#xff0c;只能说纳塔版本的boss盾真的厚&#xff0c;萌新的我去打boss&#xff0c;从白天打到黑夜&#xff0c;黑夜再打到白天&#xff08;游戏里面的时间&#xff09;。 闲话结…