java并发编程(并发编程的三个问题)

article/2025/9/17 0:00:37

什么是并发编程?

首先我们要知道什么是并发? 什么是并行?

并行: 多件事情在同一时刻同时发生

并发: 在同一时间内,多个事情交替执行

并发编程: 比如抢票,秒杀等在同一场景下,有大量的请求访问同一资源, 会出现一些安全性的问题,所以要通过编程来控制多个线程依次访问资源,称为并发编程

引发并发编程的根本原因

因为所有的java代码都是在java虚拟机中运行的, 而java虚拟机也有自己的模型-----

Java 内存模型(Java Memory Model,JMM)

Java 内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,在工作内存中操作完之后,再将数据写入主内存中.

这里的工作内存是 JMM 的一个抽象概念,也叫本地内存,其存储了该线程读 /写共享变量的副本, 就像每个处理器内核拥有私有的高速缓存,JMM 中每个线程拥有私有的本地内存。

并发编程的三个核心问题(不可见性,乱序性,非原子性)

1.不可见性

多个线程同时对共享资源进行操作, 彼此之间是不可见的,操作完之后再写入到主内存中,这样可能会出现问题,与我们预期的结果不相符

例如:

我们预期的结果应该是count=2;

然而这是不一定的, 当线程1从主内存中拿到count进行++操作后变成了1,还没来得及写入主内存时,线程2也拿到了count=0的数据进行了++操作是的其变成了1,两个线程写入主内存后最终的结果count是为1的,而不是2,也就是线程1操作了主内存中的数据,线程2并不知道.

2.无序性

为了优化性能,对一些代码指令进行了顺序重排,提高程序的速度

3.原子性

原子的意思代表着——“不可分”, 原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作, 线程切换导致了原子性问题

CPU 能保证的原子操作是 CPU 指令级别的(i++,就只有一条语句,进行一次操作),而不是高级语言的操作符(i++,有三个操作过程), 高级语言里一条语句往往需要多条 CPU 指令完成。如 count++,至少需要三条 CPU指令。

指令 1:首先,需要把变量 count 从内存加载到工作内存;

指令 2:之后,在工作内存执行 +1 操作;

指令 3:最后,将结果写入内存;

如何解决这三个问题?

1.使用volitile关键字

一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后:

1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2. 禁止进行指令重排序。

3. volatile 不能保证对变量操作的原子性。

volitile的底层实现原理

使用 Memory Barrier(内存屏障)。

内存屏障是一条指令,该指令可以对编译器(软件)和处理器(硬件)的指令重排做出一定的限制,比如,一条内存屏障指令可以禁止编译器和处理器将其后面的指令移到内存屏障指令之前。

Volatile 变量编译为汇编指令会多出Lock 前缀.

有序性实现:主要通过对 volatile 修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性的。

可见性实现:主要是通过 Lock 前缀指令 + MESI 缓存一致性协议来实现的。对 volatiile 修饰的变量执行写操作时,JVM 会发送一个 Lock 前缀指令给CPU,CPU 在执行完写操作后,会立即将新值刷新到存,同时因为 MESI 缓存一致性协议,其他各个 CPU 都会对总线嗅探,看自己本地缓存中的数据是否被别人修改,如果发现修改了,会把自己本地缓存的数据过期掉。然后这个 CPU里的线程在读取改变量时,就会从主内存里加载最新的值了,这样就保证了可见性

可见性实现的例子:

public class ThreadDemo implements  Runnable{/*volatile 修饰的变量,在一个线程中被修改后,对其它线程立即可见禁止cpu对指令重排序*/private    boolean  flag = false;//共享数据public void run() {try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}this.flag = true;//让一个线程修改共享变量值System.out.println(this.flag);}public boolean getFlag() {return flag;}public void setFlag(boolean flag) {this.flag = flag;}
}
public class TestVolatile {public static void main(String[] args) {//创建线程任务ThreadDemo td = new  ThreadDemo();Thread t = new Thread(td);//创建线程t.start();//main线程中也需要使用flag变量while(true){if(td.getFlag()){//false-trueSystem.out.println("main---------------");break;}}}
}

上面例子中,main主线程中执行一个子线程,还要执行一个while循环,只有当子线程中的flag为true时才可以跳出while循环

当共享数据flag没有被volitile修饰时,输出结果如下:

一直在死循环,没有跳出while循环,但是在子线程中flag已经改为了true,也已经输出了,说明main主线程和子线程彼此的操作时不可见性的, 但是有volitile修饰时,就会跳出循环.

有序性实现的例子:

/*模拟指令重排序*/
public class Reorder {private  volatile static int x;private  volatile static int y;private  volatile static int a;private  volatile static int b;public static void main(String[] args) throws InterruptedException {int i = 0;for(;;) {i++;x = 0; y = 0;a = 0; b = 0;Thread one = new Thread(new Runnable() {public void run() {a = 1;x = b;}});Thread other = new Thread(new Runnable() {public void run() {b = 1;y = a;}});one.start();other.start();one.join();other.join();String result = "第" + i + "次 (" + x + "," + y + ")";if(x == 0 && y == 0) {System.err.println(result);break;} else {System.out.println(result);}}}}

有volitile修饰时输出结果如下:

没有volitile修饰时:

可以看到,有volitile修饰时,代码的输出是有序的.

2.通过加锁的方式,让线程互斥执行来保证一次只有一个线程对共享资源访问,从而保证原子性.

synchronized : 关键字 修饰代码块,方法 自动获取锁,自动释放锁

ReentrantLock : 类 只能对某段代码修饰 需要手动加锁,手动释放锁.

3.在java中还提供一些原子类,在低并发情况下使用,是一种无锁实现.

原子类的原子性是通过 volatile + CAS 实现原子操作的。

AtomicInteger 类中的 value 是有 volatile 关键字修饰的,这就保证了 value的内存可见性,这为后续的 CAS 实现提供了基础。低并发情况下:使用 AtomicInteger。

4.采用CAS机制(CAS(Compare-And-Swap) 比较并交换)从而保证原子性

CAS是实现乐观锁的一种方式,是一种自旋式思想,是一种轻量级的锁机制

CAS 包含了三个操作数:

①内存值 V

②预估值 A (比较时,从内存中再次读到的值)

③更新值 B (更新后的值)

当且仅当预期值 A==V,将内存值 V=B,否则什么都不做。

这种做法的效率高于加锁,当判断不成功不能更新值时,不会阻塞,继续获得 cpu执行权,继续判断执行。

自旋思想:

第一次从主内存中获取值到工作内存中,存储作为预期值,然后将对象数据修改,将工作内存中值写入到主内存, 在写入之前需要做一个判断.用预期值与主内存中的值进行比较,如果预期值与主内存中值一致,说明没有其他线程修改, 将更新数的值,写入到主内存,如果预期值与主内存中值不一致, 说明其他线程修改了主内存的值, 这时就需要重复操作整个过程

特点:

不加锁,所有的线程都可以对共享数据操作, 适合地并发是使用,由于不加锁,其他线程不需要阻塞,效率高,但是在大并发时,不停自旋判断,导致cpu占用率高,容易导致 CPU 跑满.

ABA 问题

当某个线程将内存值由 A 改为了 B,再由 B 改为了 A。当另外一个线程使用预期值去判断时,预期值与内存值相同,当前线程的 CAS 操作无法分辨当前 V 值是否发生过变化。

如何解决ABA问题?

解决 ABA 问题的主要方式,通过使用类添加版本号,来避免 ABA 问题。如原先的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2)修改为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较,只需要比较版本号 1 和 3,即可发现该内存中的数据被更新过了


http://chatgpt.dhexx.cn/article/EqiFAaw7.shtml

相关文章

java并发编程(荣耀典藏版)

大家好 我是月夜枫,聊一聊java中的并发编程,面试工作中也许都会用到,参考了很大博主的博客,整理了很久的文章,虽然还没有全部整理完,后续慢慢更新吧。 并发编程 一、线程的基础概念 一、基础概念 1.1 进…

Java并发编程基础(一篇入门)

1 并发编程简介 1.1 什么是并发编程 所谓并发编程是指在一台处理器上 “同时” 处理多个任务。并发是在同一实体上的多个事件。多个事件在同一时间间隔发生。 并发编程,从程序设计的角度来说,是希望通过某些机制让计算机可以在一个时间段内&#xff0…

关于Java并发编程的总结和思考

编写优质的并发代码是一件难度极高的事情。Java语言从第一版本开始内置了对多线程的支持,这一点在当年是非常了不起的,但是当我们对并发编程有了更深刻的认识和更多的实践后,实现并发编程就有了更多的方案和更好的选择。本文是对并发编程的一…

Java 并发编程

目录 回顾线程 并发编程 并发编程 Java 内存模型(JMM) 编程核心问题--可见性,原子性,有序性 可见性 有序性 原子性 valatile 关键字 CAS(Compare-And-Swap,比较并交换) 原子类 java中的锁 乐观锁/悲观锁 可重用锁(…

JAVA并发编程

并发编程 1.进程与线程 进程 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。当一个…

阿里面试失败后,一气之下我图解了Java中18把锁

号外号外!《死磕 Java 并发编程》系列连载中,大家可以关注一波: 「死磕 Java 并发编程04」说说Java Atomic 原子类的实现原理 「死磕 Java 并发编程03」阿里二面,面试官:说说 Java CAS 原理? 「死磕 Jav…

Java 是否应该使用通配符导入( wildcard imports)

这个问题应该是所有使用过 Java 第一课的人都会告诉你不要使用通配符导入。 主要问题 主要的问题是它使你的本地命名空间变得混乱。 用最简单的说法就是 Date 这个对象,你可能在 java.sql.Date 和 java.util.Date 都会有这个对象。 如果你使用通配符导入的话&…

Makefile中wildcard函数的应用理解

文章目录 前言 1 "*"通配符使用场景 2 "*"通配符实例 总结 前言 如果我们想定义一系列比较类似的文件,我们很自然地就想起使用通配符。make 支持三种通配符:"*","?" 和 "[...]"。这…

DNS Wildcard(DNS泛域名)

在DNS中,泛域名(wildcard Resource Record)可以被认为是一种合成RR的机制,借助于它,DNS服务器可以响应本来不存在的域名的请求,它的设计初衷是用来把所有邮件都转发到一个邮件系统(当然&#xf…

Es 模糊查询 match,wildcard

Es 模糊查询的方式 要求: Es查询: 查询工单信息, 输入 “测试”,查出 form_name 为字段中有查询出含有符合内容的数据 match:分词模糊查询: 比如“Everything will be OK, All is well”,会被…

wildcard

[ruskyrhel7 test]$ lstest1 test123 test2 test317 test33 test335 test336 test44 testtest[ruskyrhel7 test]$ ls test?3test33[ruskyrhel7 test]$ ls test??3test123[ruskyrhel7 test]$ ls test*test1 test123 test2 test317 test33 test335 test336 tes…

Makefile中wildcard使用方法

Makefile中wildcard函数使用方法 在Makefile规则中,通配符会被自动展开。但在变量的定义和函数引用时,通配符将失效。这种情况下如果需要通配符有效,就需要使用函数“wildcard”,它的用法是:$(wildcard PATTERN…) 。…

通配符(WildCard)的使用

一、关于WildCard:一个web应用,有成千上万个action声明,可以利用struts2提供的映射机制把多个彼此相似的映射关系简化成一个映射关系,即通配符。 1.新建类 ActionWildCard,验证通配符的方法 2.1添加Student需要实践的两…

Elasticsearch 警惕使用 wildcard 检索!然后呢?

1、wildcard 检索定义 wildcard 检索可以定义为:支持通配符的模糊检索。 类似 Mysql 中的 like 模糊匹配,如下所示: Elasticsearch 中的 wildcard 使用方式如下: 通配符运算符是匹配一个或多个字符的占位符。 通配符支持两种&…

VIO你用对了吗

VIO(Virtual Input/Output)有两个主要功能 监测设计中的内部信号; 驱动设计中的内部信号。 既然是Virtual(虚拟的),就表明这个输入或输出并不是真实存在于FPGA设计中。下图显示了VIO的输入、输出管脚。其…

VIO(notes) —— (3)VIO残差构建与IMU预积分

VIO残差构建与IMU预积分 一、VIO残差函数的构建1. 系统所需的状态变量2. 视觉重投影误差2.1 视觉重投影误差2.2 逆深度参数化2.3 VIO 中基于逆深度的重投影误差 3. 预积分模型由来及意义3.1 为什么需要预积分?3.2 怎么预积分?3.3 预积分是什么&#xff1…

深蓝学院VIO课程学习笔记 VIO概述

VIO概述 1. VIO整体概述 松耦合:各部分自己算自己的,最后单独把数据来算 紧耦合:同时考虑这两个问题(效果更好) IMUGPS精度可以达到cm级,但是受环境影响比较大 融合方案 采用卡尔曼滤波,当一边…

海思3559 sample解析:vio

前言 拿到开发板,编完了平台sample,自然按捺不住要去简单学习测试了。打开最直观相对也比较简单的vio例程做个到手分析和流程梳理吧 测试 一开始自然是最磕磕绊绊的,连上HDMI线,串口登录后运行,屏幕乌漆嘛黑&#xff…

从零手写VIO(7)

从零手写VIO(7) 文章目录 从零手写VIO(7)前言一、VINS-Course代码解析二、作业(7)1.simulation-test.cpp修改2.Sysytem.cpp修改3.config文件夹下euroc_config.yaml参数修改4.param.h修改4.1无噪声4.2小噪声4.3大噪声 总结 前言 一、VINS-Course代码解析…

运行msckf_vio

1、编译 cd ~/msckf catkin_make --pkg msckf_vio --cmake-args -DCMAKE_BUILD_TYPERelease2、运行(运行euroc数据集) 首先roscore开启ros节点 cd ~/msckf source ~/msckf/devel/setup.bash roslaunch msckf_vio msckf_vio_euroc.launchcd ~/msckf sou…