并发编程的三大特性

重要通知:

本人获得了一个运维的上市公司offer,问了一下就业的老师,都推荐能去就去,因为当下的环境,我这种专业需要985211学历才比较好找,所以我要去实习了(今年挺多是实习完才给签三方),我先去给各位探探路!如果各位有和我类似的情况,学的java后端开发,在投开发岗位竞争非常激烈的情况下,不妨试试实、运维、测试岗位!

一、原子性

1.1 什么是并发编程的原子性

JMM(Java Memory Model)。不同的硬件和不同的操作系统在内存上的操作有一定差异的。Java为了解决相同代码在不同操作系统上出现的各种问题,用JMM屏蔽掉各种硬件和操作系统带来的差异。

让Java的并发编程可以做到跨平台。

JMM规定所有变量都会存储在主内存中,在操作的时候,需要从主内存中复制一份到线程内存(CPU内存),在线程内部做计算。然后再写回主内存中(不一定!)。

原子性的定义:原子性指一个操作是不可分割的,不可中断的,一个线程在执行时,另一个线程不会影响到他。

并发编程的原子性用代码阐述:

private static int count;
​
public static void increment(){try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}count++;
}
​
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 100; i++) {increment();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 100; i++) {increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);
}

当前程序:多线程操作共享数据时,预期的结果,与最终的结果不符。

原子性:多线程操作临界资源,预期的结果与最终结果一致。

通过对这个程序的分析,可以查看出,++的操作,一共分为了三部,首先是线程从主内存拿到数据保存到CPU的寄存器中,然后在寄存器中进行+1操作,最终将结果写回到主内存当中。

1.2 保证并发编程的原子性

1.2.1 synchronized

因为++操作可以从指令中查看到

image.png

可以在方法上追加synchronized关键字或者采用同步代码块的形式来保证原子性

synchronized可以让避免多线程同时操作临街资源,同一时间点,只会有一个线程正在操作临界资源

image.png

1.2.2 CAS

到底什么是CAS

compare and swap也就是比较和交换,他是一条CPU的并发原语。

他在替换内存的某个位置的值时,首先查看内存中的值与预期值是否一致,如果一致,执行替换操作。这个操作是一个原子性操作。

Java中基于Unsafe的类提供了对CAS的操作的方法,JVM会帮助我们将方法实现CAS汇编指令。

但是要清楚CAS只是比较和交换,在获取原值的这个操作上,需要你自己实现。

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

Doug Lea在CAS的基础上帮助我们实现了一些原子类,其中就包括现在看到的AtomicInteger,还有其他很多原子类……

CAS的缺点:CAS只能保证对一个变量的操作是原子性的,无法实现对多行代码实现原子性。

CAS的问题

  • ABA问题:问题如下,可以引入版本号的方式,来解决ABA的问题。Java中提供了一个类在CAS时,针对各个版本追加版本号的操作。 AtomicStampeReference

    image.png

  • AtomicStampedReference在CAS时,不但会判断原值,还会比较版本信息。

  • public static void main(String[] args) {AtomicStampedReference<String> reference = new AtomicStampedReference<>("AAA",1);
    ​String oldValue = reference.getReference();int oldVersion = reference.getStamp();
    ​boolean b = reference.compareAndSet(oldValue, "B", oldVersion, oldVersion + 1);System.out.println("修改1版本的:" + b);
    ​boolean c = reference.compareAndSet("B", "C", 1, 1 + 1);System.out.println("修改2版本的:" + c);
    }

  • 自旋时间过长问题

    • 可以指定CAS一共循环多少次,如果超过这个次数,直接失败/或者挂起线程。(自旋锁、自适应自旋锁)

    • 可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,再返回最后的结果。

1.2.3 Lock锁

Lock锁是在JDK1.5由Doug Lea研发的,他的性能相比synchronized在JDK1.5的时期,性能好了很多多,但是在JDK1.6对synchronized优化之后,性能相差不大,但是如果涉及并发比较多时,推荐ReentrantLock锁,性能会更好。

实现方式:

private static int count;
​
private static ReentrantLock lock = new ReentrantLock();
​
public static void increment()  {lock.lock();try {count++;try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}} finally {lock.unlock();}
​
​
}
​
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 100; i++) {increment();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 100; i++) {increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);
}

ReentrantLock可以直接对比synchronized,在功能上来说,都是锁。

但是ReentrantLock的功能性相比synchronized更丰富。

ReentrantLock底层是基于AQS实现的,有一个基于CAS维护的state变量来实现锁的操作。

1.2.4 ThreadLocal

Java中的四种引用类型

Java中的使用引用类型分别是强,软,弱,虚

User user = new User();

在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它始终处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。

SoftReference

其次是软引用,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中,作为缓存使用。

然后是弱引用,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。可以解决内存泄漏问题,ThreadLocal就是基于弱引用解决内存泄漏的问题。

最后是虚引用,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。不过在开发中,我们用的更多的还是强引用。

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实现原理:

  • 每个Thread中都存储着一个成员变量,ThreadLocalMap

  • ThreadLocal本身不存储数据,像是一个工具类,基于ThreadLocal去操作ThreadLocalMap

  • ThreadLocalMap本身就是基于Entry[]实现的,因为一个线程可以绑定多个ThreadLocal,这样一来,可能需要存储多个数据,所以采用Entry[]的形式实现。

  • 每一个现有都自己独立的ThreadLocalMap,再基于ThreadLocal对象本身作为key,对value进行存取

  • ThreadLocalMap的key是一个弱引用,弱引用的特点是,即便有弱引用,在GC时,也必须被回收。这里是为了在ThreadLocal对象失去引用后,如果key的引用是强引用,会导致ThreadLocal对象无法被回收

ThreadLocal内存泄漏问题:

  • 如果ThreadLocal引用丢失,key因为弱引用会被GC回收掉,如果同时线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同时也无法被获取到。

  • 只需要在使用完毕ThreadLocal对象之后,及时的调用remove方法,移除Entry即可

image.png

二、可见性

2.1 什么是可见性

可见性问题是基于CPU位置出现的,CPU处理速度非常快,相对CPU来说,去主内存获取数据这个事情太慢了,CPU就提供了L1,L2,L3的三级缓存,每次去主内存拿完数据后,就会存储到CPU的三级缓存,每次去三级缓存拿数据,效率肯定会提升。

这就带来了问题,现在CPU都是多核,每个线程的工作内存(CPU三级缓存)都是独立的,会告知每个线程中做修改时,只改自己的工作内存,没有及时的同步到主内存,导致数据不一致问题。

image.png

可见性问题的代码逻辑

private 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");
}

2.2 解决可见性的方式

2.2.1 volatile

volatile是一个关键字,用来修饰成员变量。

如果属性被volatile修饰,相当于会告诉CPU,对当前属性的操作,不允许使用CPU的缓存,必须去和主内存操作

volatile的内存语义:

  • volatile属性被写:当写一个volatile变量,JMM会将当前线程对应的CPU缓存及时的刷新到主内存中

  • volatile属性被读:当读一个volatile变量,JMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变量

其实加了volatile就是告知CPU,对当前属性的读写操作,不允许使用CPU缓存,加了volatile修饰的属性,会在转为汇编之后后,追加一个lock的前缀,CPU执行这个指令时,如果带有lock前缀会做两个事情:

  • 将当前处理器缓存行的数据写回到主内存

  • 这个写回的数据,在其他的CPU内核的缓存中,直接无效。

总结:volatile就是让CPU每次操作这个数据时,必须立即同步到主内存,以及从主内存读取数据。

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");
}
2.2.2 synchronized

synchronized也是可以解决可见性问题的,synchronized的内存语义。

如果涉及到了synchronized的同步代码块或者是同步方法,获取锁资源之后,将内部涉及到的变量从CPU缓存中移除,必须去主内存中重新拿数据,而且在释放锁之后,会立即将CPU缓存中的数据同步到主内存。

private static boolean flag = true;
​
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (flag) {synchronized (MiTest.class){//...}System.out.println(111);}System.out.println("t1线程结束");
​});
​t1.start();Thread.sleep(10);flag = false;System.out.println("主线程将flag改为false");
}
2.2.3 Lock

Lock锁保证可见性的方式和synchronized完全不同,synchronized基于他的内存语义,在获取锁和释放锁时,对CPU缓存做一个同步到主内存的操作。

Lock锁是基于volatile实现的。Lock锁内部再进行加锁和释放锁时,会对一个由volatile修饰的state属性进行加减操作。

如果对volatile修饰的属性进行写操作,CPU会执行带有lock前缀的指令,CPU会将修改的数据,从CPU缓存立即同步到主内存,同时也会将其他的属性也立即同步到主内存中。还会将其他CPU缓存行中的这个数据设置为无效,必须重新从主内存中拉取。

private static boolean flag = true;
private static Lock lock = new ReentrantLock();
​
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (flag) {lock.lock();try{//...}finally {lock.unlock();}}System.out.println("t1线程结束");
​});
​t1.start();Thread.sleep(10);flag = false;System.out.println("主线程将flag改为false");
}
2.2.4 final

final修饰的属性,在运行期间是不允许修改的,这样一来,就间接的保证了可见性,所有多线程读取final属性,值肯定是一样。

final并不是说每次取数据从主内存读取,他没有这个必要,而且final和volatile是不允许同时修饰一个属性的

final修饰的内容已经不允许再次被写了,而volatile是保证每次读写数据去主内存读取,并且volatile会影响一定的性能,就不需要同时修饰。

image.png

三、有序性

3.1 什么是有序性

在Java中,.java文件中的内容会被编译,在执行前需要再次转为CPU可以识别的指令,CPU在执行这些指令时,为了提升执行效率,在不影响最终结果的前提下(满足一些要求),会对指令进行重排。

指令乱序执行的原因,是为了尽可能的发挥CPU的性能。

Java中的程序是乱序执行的。

Java程序验证乱序执行效果:

static int a,b,x,y;
​
public static void main(String[] args) throws InterruptedException {for (int i = 0; i < Integer.MAX_VALUE; i++) {a = 0;b = 0;x = 0;y = 0;
​Thread t1 = new Thread(() -> {a = 1;x = b;});Thread t2 = new Thread(() -> {b = 1;y = a;});
​t1.start();t2.start();t1.join();t2.join();
​if(x == 0 && y == 0){System.out.println("第" + i + "次,x = "+ x + ",y = " + y);}}
}

单例模式由于指令重排序可能会出现问题:

线程可能会拿到没有初始化的对象,导致在使用时,可能由于内部属性为默认值,导致出现一些不必要的问题

private static volatile MiTest test;
​
private MiTest(){}
​
public static MiTest getInstance(){// Bif(test  == null){synchronized (MiTest.class){
​if(test == null){// A   ,  开辟空间,test指向地址,初始化test = new MiTest();}}}return test;
}

3.2 as-if-serial

as-if-serial语义:

不论指定如何重排序,需要保证单线程的程序执行结果是不变的。

而且如果存在依赖的关系,那么也不可以做指令重排。

// 这种情况肯定不能做指令重排序
int i = 0;
i++;
​
// 这种情况肯定不能做指令重排序
int j = 200;
j * 100;
j + 100;
// 这里即便出现了指令重排,也不可以影响最终的结果,20100

3.3 happens-before

具体规则:

  1. 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。

  2. 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。

  3. volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操作。

  4. happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。

  5. 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。

  6. 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。

  7. 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。

  8. 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。 JMM只有在不出现上述8中情况时,才不会触发指令重排效果。

不需要过分的关注happens-before原则,只需要可以写出线程安全的代码就可以了。

3.4 volatile

如果需要让程序对某一个属性的操作不出现指令重排,除了满足happens-before原则之外,还可以基于volatile修饰属性,从而对这个属性的操作,就不会出现指令重排的问题了。

volatile如何实现的禁止指令重排?

内存屏障概念。将内存屏障看成一条指令。

会在两个操作之间,添加上一道指令,这个指令就可以避免上下执行的其他指令进行重排序。

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

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

相关文章

SXSSFWorkbook-MinIo-大数据-流式导出

文章目录 前言业务现状架构思路技术细节生成摘要IDSXSSFWorkbookMinIomybatis 流查询PipedInputStream 保存到minio 总结 前言 由于业务涉及到数据比较大&#xff0c;用户对导出功能使用频繁&#xff0c;每次导出数据两10万以上。 为了减少数据库压力&#xff0c;及应用服务器…

docker搭建rocketmq集群

单机搭建 1 拉取rocketMq镜像 docker pull rocketmqinc/rocketmq:4.3.2 2 创建挂在目录 mkdir -p /mydata/rocketmq/data/namesrv/logs /mydata/rocketmq/data/namesrv/store mkdir -p /mydata/rocketmq/data/broker/logs /mydata/rocketmq/data/broker/store mkd…

使用LLM在KG上进行复杂的逻辑推理10.12

使用LLM在KG上进行复杂的逻辑推理 摘要介绍相关工作 摘要 在知识图谱上进行推理是一项具有挑战性的任务&#xff0c;这需要深度理解实体之间复杂的关系和它们关系的逻辑。而当前的方法通常依赖于学习 几何形状 以将实体嵌入到向量空间中进行逻辑查询操作&#xff0c;但在复杂查…

必知必会的22种设计模式(GO语言)

日常工作中免不了使用设计模式&#xff0c;那么你使用了哪些设计模式呢&#xff1f; 设计模式是什么&#xff1f; 设计模式是一种在软件设计中对常见问题的通用解决方案。 它们是经过验证的、可重用的设计思想&#xff0c;可以帮助解决开发过程中遇到的各种问题。 设计模式…

三次挥手和四次握手

TCP建立连接&#xff08;三次握手&#xff09; 经过DNS域名解析后&#xff0c;获取到了服务器的IP地址&#xff0c;在获取到IP地址后&#xff0c;便会开始建立一次连接&#xff0c;这是由TCP协议完成的&#xff0c;主要通过三次握手进行连接。 第一次握手&#xff1a; 建立连…

整理mongodb文档:副本集成员可以为偶数

个人博客 整理mongodb文档:副本集成员可以为偶数 想了下&#xff0c;仲裁节点还是不想直接说太多&#xff0c;怕有的同学想太多&#xff0c;且本身副本集就偏向运维的&#xff0c;新手基本也没什么权限操作&#xff0c;就不多废话了。 文章概叙 文章从MongoDB是否可以用偶数…

Hi3516DV500 SVP_NNN添加opencv库记录

默认没有带opencv库&#xff0c;但是实际项目中需要用到opencv库&#xff0c;因此添加一下此库&#xff1b; 1&#xff1a;编译opencv源码&#xff0c;这里具体可以参考 海思Hi3516移植opencv以及错误调试_海思hi3516摄像头开发-CSDN博客 2&#xff1a;在工程的根目录下新建…

地下城堡3魂之诗阵容搭配攻略

在地下城堡3魂之诗游戏中&#xff0c;拥有一个合理搭配的阵容非常关键&#xff0c;可以让角色能力发挥最大化。以下是建议的阵容搭配及攻略&#xff1a; 关注【娱乐天梯】&#xff0c;获取内部福利号 1.核心成员(2名) 在阵容中选择两个输出型角色作为核心成员&#xff0c;他们的…

SettingsView/设置页 的实现

1. 创建设置视图 SettingsView.swift import SwiftUI/// 设置页面 struct SettingsView: View {/// 环境变量&#xff0c;呈现方式&#xff1a;显示或者关闭Environment(\.presentationMode) var presentationMode/// 默认网址let defaultURL URL(string: "https://www.…

v-model绑定input、textarea、checkbox、radio、select

1.input <div><!-- v-model绑定input --><input type"text" v-model"message"><h2>{{message}}</h2></div><script>const App{template:#my-app,data() {return {message:Hello World,}},}Vue.createApp(App).…

扁圆头带榫螺栓

声明 本文是学习GB-T 15-2013 扁圆头带榫螺栓. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本标准规定了螺纹规格为M6&#xff5e;M24、 产品等级为C 级的扁圆头带榫螺栓。 2 规范性引用文件 下列文件对于本文件的应用是必不可少的。凡是…

IP真人识别方法与代理IP检测技术

随着互联网的发展&#xff0c;IP地址在网络安全和数据分析中扮演着重要的角色。为了维护网络的安全性和识别真实用户&#xff0c;IP地址的真实性和来源成为了一个关键问题。 什么是IP真人识别&#xff1f; IP真人识别是一种技术&#xff0c;旨在确定IP地址背后的用户是否为真实…

【Node.js】路由

基础使用 写法一&#xff1a; // server.js const http require(http); const fs require(fs); const route require(./route) http.createServer(function (req, res) {const myURL new URL(req.url, http://127.0.0.1)route(res, myURL.pathname)res.end() }).listen…

Java内存空间(学习随笔)

1、程序运行中栈可能会出现两种错误 StackOverFlowError&#xff1a; 若栈的内存大小不允许动态扩展&#xff0c;那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候&#xff0c;就抛出 StackOverFlowError 错误。OutOfMemoryError&#xff1a; 如果栈的内存大小可…

Zookeeper-JavaApI操作

JavaApI操作 JavaApI操作1) Curator 介绍2) Curator API 常用操作a) 建立连接与CRUD基本操作b) Watch事件监听c) 分布式锁c.1) 介绍c.2) Zookeeper分布式锁原理c.3) 案例&#xff1a;模拟12306售票 JavaApI操作 1) Curator 介绍 Curator 是 Apache ZooKeeper 的Java客户端库。…

工作杂记-YUV的dump和read

工作小记-YUV的dump和read 工作杂记-YUV的dump和read利用dump生成图片 yuv2imgyuv2img代码 工作杂记-YUV的dump和read 工作中涉及到模型验证相关的工作&#xff0c;这里是三个模型的共同作用&#xff0c;在感知模型读取图片的时候&#xff0c;把输入替换成自己给定的输入&…

STM32--WDG看门狗

文章目录 WDG简介IWDGIWDG的超时计算WWDGWWDG超时和窗口值设定独立看门狗工程WWDG工程 WDG简介 WDG看门狗&#xff08;Watchdog Timer&#xff09;是一种常见的硬件设备&#xff0c;在STM32F10系列中&#xff0c;有两种看门狗&#xff0c;分别是独立看门狗和窗口看门狗&#x…

Contextual Transformer Networks for Visual Recognition

Contextual Transformer Networks for Visual Recognition 1. 摘要2. 目的3. 网络设计 代码地址 1. 摘要 Transformer with self-attention has led to the revolutionizing of natural language processing field, and recently inspires the emergence of Transformer-style …

基于VUE的图书借阅管理系统的设计与实现

目录 一、摘要 二、技术描述 三、部分截图 四、获取方式 一、摘要 随着我国经济的高速发展&#xff0c;人们对图书的需求也愈发旺盛&#xff0c;而传统图书管理模式存在以下弊端&#xff1a;信息存储和分类操作不够高效&#xff0c;导致查找书籍困难&#xff1b;借还书流程…

Floorplanning with Graph Attention

Floorplanning with Graph Attention DAC ’22 目录 Floorplanning with Graph Attention摘要1.简介2.相关工作3.问题公式化4. FLORA的方法4.1 解决方案概述4.2 C-谱聚类算法 4.3 基于GAT的模型4.4 合成训练数据集生成 摘要 布图规划一直是一个关键的物理设计任务&#xff0…