【并发编程之美】并发编程介绍

news/2024/5/17 18:15:32 标签: 并发编程, 多线程, 线程安全, volatile, synchronized

什么是多线程并发编程

并发:是指同一个时间段内多个任务同事都在执行,并且没有执行结束,而且是在单位时间内多个任务同时在执行。

并发强调的时在同一个时间段内同时执行多个任务,所以在单cpu的时候会根据时间片来进行执行,执行完一个时间片之后切换到下一个进程的时间片再去进行执行,以此来觉得是同时执行,而实际上每个cpu只会干一件事。

在多cpu的时候,会每个线程在一个cpu中执行,这时候每个线程在自己的cpu上进行工作,互不干扰,这才是真正的并行运行。

为什么要进行多线程并发编程

多个cpu意味着每个线程可以使用自己的cpu运行,减少了线程上下文切换的开销,但随着对应用系统性能和吞吐量要求的提高,出现了处理海量数据和请求的要求,这些都对高并发编程有着迫切的需求。

JAVA中的线程安全问题

线程安全是只当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题。

共享资源:一个资源被多个线程所持有或者说时被多个线程所访问,就叫共享资源。

线程不安全的例子

主内存中现在有两个线程,线程A和线程B,现在这两个线程同时执行对内存中的变量进行加一操作,但是此时就可能会遇到线程安全的问题。
在这里插入图片描述
主内存里面的变量现在的值为0,线程A对count进行加一,线程B也对count进行加一,当然我们最后想要的值肯是2,但是如果线程不安全的话,可能最后的值就会有问题。

我们对count进行加一的操作需要有三步操作:1,获取count的值,2,进行加一,3,把count的值存回去。

首先,线程A获取到count的值为0,然后线程A正在执行加一操作,同时线程B也去主内存中获取了count的值,此时线程A还没有将加一的结果赋给主内存的count,所以此时就会出现这种情况:
在这里插入图片描述
现在线程A要将自己的值保存到主内存中,此时主内存中count的值为1,然后线程B对count加一
在这里插入图片描述
线程B将count的运算结果保存到主内存中

最后得到的结果,count=1,这个结果是错误的,实际上得到的结果应该是2。

用时间节点来表示一下:
在这里插入图片描述
所以这里就涉及到一个多线程共享变量的可见性问题

synchronized_35">synchronized关键字

synchronized块是java提供的一种原子性内治所,java中的每个对象都可以把它当作一个同步锁来使用,这些java内置的使用者看不到的锁被称为内置锁,也叫做监视器锁。
synchronized所标注的代码块会自动获取内部锁,然后其他访问当前代码块的线程会被阻塞挂起。内置锁是排他锁,其他线程必须等待该线程释放锁后才能获取锁。
由于java中的线程和操作系统中的线程是一一对应的,所以阻塞线程需要进行线程切换,从用户态切换到内核态执行阻塞操作,这个过程是非常耗时的,而且synchronized又会导致线程的阻塞,进而导致线程的上下文切换并带来线程调度开销。

synchronized可以解决上面的共享变量内存可见性问题。

volatile_41">volatile关键字

volatile是怎样实现了?比如一个很简单的Java代码:

instance = new Instancce() //instance是volatile变量

在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令(具体的大家可以使用一些工具去看一下,这里我就只把结果说出来)。我们想这个Lock指令肯定有神奇的地方,那么Lock前缀的指令在多核处理器下会发现什么事情了?主要有这两个方面的影响:

  • 将当前处理器缓存行的数据写回系统内存;
  • 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。因此,经过分析我们可以得出如下结论:

  • Lock前缀的指令会引起处理器缓存写回内存;
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
  • 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。

这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值。

volatile_59">在什么时候使用volatile关键字

  • 写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取——计算——写入散步操作,这三步操作不是原子性的,而volatile不保证原子性。
  • 读写变量值时没有加锁。因为锁本身已经保证了内存可见性,这时候不需要把变量声明为volatile的。

参考

彻底理解volatile - 掘金
Java 理论与实践: 正确使用 Volatile 变量


http://www.niftyadmin.cn/n/1684218.html

相关文章

【Python实战系列】串口实时接收数据并基于pyqtgraph绘图

0、前言 串口数据图形化调试助手,这乍一听起来貌似还挺阔以的样子。那究竟是啥子呢?且听慢慢道来。事情的缘由是当前在做的一个项目中,在调试Mag(地磁)相关的Sensor。获取Mag的原始数据之后,首先要做的就是判断原始数据的质量如何…

【C语言】宏定义:#define NUM (M+1)*M/2怎么运算的?

前些天朋友问我一个问题是关于宏定义的运算的,内容大致关于如下的宏定义的: #define N 2 #define M N1 #define NUM (M1)*M/2Q:为什么NUM不是6呢?你看N2,M3,那么NUM不就是(31)*3/2嘛。小学一年级level的算…

【深入理解JVM】(三)垃圾回收机制

1 概述 垃圾回收机制(Garbage COllection,GC),其实这不是JAVA语言所独有的技术,在1960年诞生于MIT的Lisp是第一门使用了GC的语言,在那个时候人们就开始思考了GC需要做的这三件事情: 那些内存需…

IAP与APP合并为一个烧写文件-STM32实测

0、前言 之前写过一个小专题是基于STM32有关IAP的应用的专题,现在新增一个番外篇是关于如何将IAP与APP合并为一个烧录文件及相关实例。 第一章:浅析ICP与ISP、及IAP三种单片机烧录方式 第二章:STM32应用IAP进行程序更新详解及实例 第三章&…

【spring cloud】(一)Eureka中的服务注册与发现

1 服务治理 服务治理是微服务架构中最为核心和基础的模块,主要来实现各个微服务实例的自动化注册与发现。为了解决微服务架构中的服务实例维护问题,引入了eureka来进行维护,同时也简化的这些服务之间的关系,更加简单方便。 服务…

【spring cloud】(二)保证Eureka高可用的集群搭建

1 Eureka集群的高可用 为了保证服务的高可用,所以需要对Eureka进行一个集群的搭建,用以保证Eureka的稳定,同时防止各种意外宕机事故的发生。 在没有使用集群之前Server端只有一个,Client端也只有一个,这时我们为了保证…

【C语言】花式操作之container_of()宏的用法

0 、前言 前段时间在做算法移植的时候遇到一个问题,源头直指一个名为container_of()的函数,该函数的花式写法也着实让人不禁多看几眼。该宏的定义如下: #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)#define cont…

docker的架构图如何画

如何画docker的架构图 拥有全局的思维利用好图像处理能力高人指路 拥有全局的思维 对于docker的学习是这样,其实不仅仅是对于docker,这里只是对docker学习做一个例子。 首要任务是要明确为什么。docker能给我们带来什么样的好处,能给我们…