可见性、有序性、原子性

Volatile在C/C++和JAVA中的区别

在C/C++和Java中,volatile修饰符的含义有所不同:

可见性定义:当前线程修改了此值,其他线程立即产生变化或者获取最新的结果

C/C++语言中volatile关键字的作用

    volatile的本意是"易变的" 因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据

    当要求使用volatile声明变量值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据

在C/C++中,volatile修饰符指示编译器不要对被修饰的对象进行优化。它主要用于多线程编程场景下,保证变量的可见性,并防止编译器对变量进行过度优化。

    当一个对象被声明为volatile时,在每次访问该对象时都会从内存中读取最新值,而不是使用寄存器中的缓存值

Java关键字Volitale的作用

    可见性:保证了新值能立即存储到主内存,每次使用前立即从主内存中刷新。

    有序性:禁止指令重排序优化。

在Java中,volatile关键字也用于实现多线程编程时确保共享变量的可见性。与C/C++类似,将一个变量声明为volatile后,任何对该变量的修改都会立即写入主内存,并且其他线程读取该变量时会直接从主内存获取最新值。

    此外,在Java中,使用volatile还可以禁止指令重排序

需要注意的是,在C/C++中使用volatile并不能完全解决多线程编程问题,因为它只保证了可见性,并没有提供原子性和顺序性保证

而在Java中,虽然 volatile提供了一些原子操作的特性,但它仍然无法替代更强大的synchronized或者 java.util.concurrent.atomic.Atomic* 类来实现复杂的同步需求。

并发编程的三大特性

并发编程的三大特性:可见性、有序性、原子性

进程: 静态概念,资源分配的最小单位

线程: 动态概念,main是主线程。线程是资源调度的最小单位。进程里包含线程,所有线程共享所属进程的资源。

CPU在执行线程时并不会直接和内存打交道,因为从内存上读数据往往是很慢的,它会把要用到的数据存储在寄存器(Registers)上,这样访问速度会非常快。ALU是计算单元,PC是程序计数器,由于Registers大小并不大,但是为了条效率我们往往会希望它大一点,解决方案就是个Registers加一些缓存以扩大存储空间。

CPU在切换线程时,会将上一个线程的Registers信息和PC信息缓存到一个cache中,之后再切换为另一个线程。切换回去就会从cache中再把原来线程的信息拿回来,反复地恢复现场。一个程序中线程数并不是越多越好,线程在切换时很消耗CPU资源,如果线程太多,CPU会把精力投入到切换线程中,从而降低了程序执行速度。

单核CPU未经过超线程处理时,执行多线程程序有没有意义?

有,即便是只有单核CPU,在一些比较耗时的操作例如等待网络响应,等待IO等操作时,CPU也是处于待机阶段,如果在这段时间里,有其他线程可以把这个空闲时间利用起来,CPU的利用效率是特别高的,会比单线程执行更快。

超线程

    一个CPU中的ALU对应多个Registers和PC。

CPU缓存机制

    CPU访问内存非常慢,CPU的运行速度和访问内存的速度比为:1:100。所以CPU一般都先喝缓存打交道,缓存里没有最后才去内存里找。一般会在寄存器和内存间设置三级缓存L1,L2,L3。多核CPU中,L1,L2独立存在于每个核中,L3则存在于CPU中,即两个核共享一个L3。CPU访问数据的顺序是:L1->L2->L3->内存;同样,写入数据的顺序也一样。每次写入和读取并不是单个数据,而是一行数据,行以缓存行计算,一行缓存行64字节。如果要用的数据不存在缓存中,则会访问内存再读64字节数据进来。按行读取是为了提高缓存命中率,说不定我这次直接读到了一会儿要用的数据,这样CPU访问缓存速度就会很快,即便没命中也没关系。

可见性

    可见性定义:当前线程修改了此值,其他线程立即产生变化或者获取最新的结果。

    CPU的缓存机制导致线程在修改变量和读取变量时,这些变量值只会存放在缓存中,并不会立刻更新到内存,什么时候更新到内存?这个时间是不一定的,某些指令会触发数据更新到内存的操作,有些操作不会。假设没有触发这些指令,那么线程A修改的数据就一直存放在它的L1中,不会修改内存。这就是可见性。我修改了数据但别的线程看不见

如何解决可见性问题

    volatile关键字可以完成对修饰变量可见性的保证(C/C++和JAVA的volatile关键字都可以),其底层实现原理是利用了底层的"lock"命令,lock命令拥有三个作用:

    (1) 用于在多处理器中执行指令时对共享内存的独占。也就是锁住总线,等我改完了再放开

    (2) 将当前处理器对应缓存的内容刷新到内存,并且其他处理器对应的缓存失效所以执行"lock"命令会使其他线程重新刷新自己的缓存,重新从内存读取一遍。这样就保证了可见性

    (3) 使有序的指令无法越过这个内存屏障,防止指令重排(JAVA才有,C/C++没有)

有序性

    程序真的是按顺序执行吗?程序不一定是按照顺序执行的,为了提高CPU的利用率,让CPU尽可能多的工作,CPU会在某些语句当中选择"乱序执行"。例如,当CPU遇到一个IO操作或者从主存中读取数据的操作,这两个操作是十分耗时的而且CPU会进入等待,为了让CPU在这一段等待时间里也能被利用起来。CPU会选择在等待时间先进行其他与这次等待操作无关的其他操作。所以,程序往往并不是按顺序执行的。

    指令重排发生的概率是很大的。指令重排是存在于汇编语言上的。

CPU乱序的前提条件

    如果任何语句都可以发生指令重排,那程序就炸裂了,CPU对指令重排有着严格的规定,防止在有逻辑关系的语句中发生指令重排影响最终结果。

CPU拥有as-if-serial规则:可以乱序执行不影响单线程的最终一致性的代码,存在依赖关系的代码不能被乱序执行CPU保护的只是单线程的最终一致性,也就是说,CPU不会保护多线程的最终一致性

如何防止指令重排

使用内存屏障,拥有两个层次:

(1)CPU层:屏障在汇编语言中是一种特殊指令,遇到这种特殊指令,前面的内容必须执行完才可以执行接下来的指令。一般有如下指令:Ifence,sfence,mfence(inter中的)等。这些指令在各各品牌的CPU中不统一。

(2) JVM层:JVM通过volatile关键字禁止指令重排。实现一个JVM虚拟机,需要从原语层面上实现如下四种内存屏障:

1> LoadLoad屏障

       对于Load1;LoadLoad;Load2;这样的语句,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

    2> StoreStore屏障

       对于Store1;StoreStore;Store2;这样的语句,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

    3> LoadStore屏障

       对于Load1;LoadStore;Store2;这样的语句,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

    4> StoreLoad屏障

       对于Store1;StoreLoad;Load2;这样的语句,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

    其中Load代表读,Store代表写,LoadStore屏障就代表:屏障前的这条Load读语句不可以和屏障后面的这条Store写语句执行顺序互换,其他三种屏障以此类推。

被volatile关键字(JAVA的)修饰的变量,在对其内存空间进行读写操作时遵循如下规则:在写入指令前加一个SS屏障,在写入指令后加一个SL屏障。让前面的写入指令全部写完自己再执行,自己写入执行完后面的读指令再读。在读指令后加一个LL屏障和LS屏障,让自己读完其他指令再读,读完之后,其他指令再写。

原子性

    原子性是指,一段逻辑操作不能被打断。必须一口气执行完毕,过程中不能被影响

    在并发环境下最常见的就是A线程和B线程同时执行一段代码逻辑,这段逻辑中存在共享资源。二者因为CPU的调度问题,互相打断导致共享资源与预期逻辑不一致的问题。原子性就是要保证 A和B线程在执行过程中,不能被对方打断,要一口气把自己的逻辑全部跑完。

CAS

    Compare and swap和Compare and exchange两个操作。

    CAS在修改数据的值时,会首先读一下要修改数据的原值,之后在执行修改操作前看看刚才读的值和内存中现在存储的值相不相同,如果不相同,返回false,不进行操作。如果相同,则返回true,进行修改操作。

    通过这样的方式来达到确认自己是否竞争成功。本质上与锁不同。

CAS底层实现

    CAS的实现在CPU底层上有一条指令效果与它想要的一致,所以从底层上是被支持的。底层使用lock cmpxchg这条指令,在执行CAS时锁住数据传输的总线,保证原子性

    CAS一般用于自旋锁Eg: while(CAS){ if(CAS){ 修改操作 } },利用CAS方法会返回true和false,利用这个判断进行原子性操作。

    CAS与锁本质上不同,锁的竞争和加锁解锁需要消耗资源,效率较低。而CAS作为纯内存操作,效率较高,但是CAS在大量竞争时会很消耗CPU。CAS的效率要高于锁的

锁是一种悲观的机制。为多线程提供了互斥的访问机制。多个线程同时竞争锁时,没获得锁的线程将会被挂起。

锁的劣势

(1)未竞争到锁的线程挂起后再恢复时,会进行上下文的切换,开销大。

(2)当一个线程正在等待锁时,它不能干任何其它事情。

    如果持有锁的线程被延迟执行(例如发生了缺页错误、调度延迟、或者其它类似情况),那么所有需要该锁的线程都必须等待下去。

    如果被阻塞线程的优先级较高,而持有锁的线程优先级较低,那么这将是一个严重的问题——优先级反转。即使高优先级的线程可以抢先执行,但仍然需要等待锁被释放,从而导致它的优先级会降至低优先级线程的级别。

    如果持有锁的线程被永久阻塞(例如由于出现无限循环,死锁,活锁或者其他的活跃性障碍),所有等待这个锁的线程就永远无法执行下去。

volatile是一种更轻量级的同步机制。

volatile的优势:在使用这些变量时,不会发生上下文切换和线程调度等操作。

volatile的局限:不能保证复合操作的原子性。例如i++,并不能通过volatile来保证原子性(i++包含了读取i的当前值,对其进行加1操作,然后将结果写回这样的多个步骤,这个过程发生了线程切换或者其他并发操作,就有可能导致竞态条件)

对于细粒度操作,除了volatile提供的轻量级的同步机制,还有另外一种更高效的乐观方法。在针对多处理器操作而设计的处理器中提供了一些特殊指令,用于管理对共享数据的并发访问。现在,几乎所有的现代处理器中都包含了某种形式的原子读-改-写指令,例如比较并交换(compare and swap)、关联加载/条件存储(load linked/store conditional)。

CAS是compare and swap, 简单来说就是,在写入新值之前,读出旧值,当且仅当旧值与存储中的当前值一致时,才把新值写入存储

    CAS操作包括三个操作数:内存位置(V)、预期原值(A)和新值(B)。

    当执行CAS操作时,只有当V的值等于A时,才会将V的值更新为B,否则不做任何操作

    CAS操作是原子性的,也就是说在同一时刻只能有一个线程执行CAS操作,因此CAS机制保证了数据的一致性

    CAS是一种乐观锁机制,也被称为无锁机制。全称: Compare-And-Swap。它是并发编程中的一种原子操作,通常用于多线程环境下实现同步和线程安全。CAS操作通过比较内存中的值与期望值是否相等来确定是否执行交换操作。如果相等,则执行交换操作,否则不执行。由于CAS是一种无锁机制,因此它避免了使用传统锁所带来的性能开销和死锁问题,提高了程序的并发性能。

    在并发编程中,当多个线程同时访问共享资源时,如果不进行同步控制,就会出现数据不一致的情况。传统的同步机制包括使用锁等。这些机制虽然可以保证数据一致性和线程安全性,但也存在一些问题,比如锁的开销和线程阻塞等,导致程序的并发性能受到影响。

    而CAS乐观锁机制则是一种不使用锁的同步机制,它避免了锁机制的开销和线程阻塞,提高了并发性能

    CAS操作通过比较内存中的值与期望值是否相等来确定是否执行交换操作。如果相等,则执行交换操作,否则不执行。由于CAS是一种乐观的机制,它避免了线程的阻塞,提高了程序的并发性能。因此,CAS乐观锁在并发编程中具有重要的作用,它可以提高系统的并发性能和吞吐量,同时保证数据的一致性和线程安全性。

对于gcc、g++编译器来讲,它们提供了一组API来做原子操作:

 (1) 这些内建执行名称建议的操作,并返回以前在内存中的值;

 type __sync_fetch_and_add (type *ptr, type value, ...)

 type __sync_fetch_and_sub (type *ptr, type value, ...)

 type __sync_fetch_and_or (type *ptr, type value, ...)

 type __sync_fetch_and_and (type *ptr, type value, ...)

 type __sync_fetch_and_xor (type *ptr, type value, ...)

 type __sync_fetch_and_nand (type *ptr, type value, ...)

 (2) 这些内建执行名称建议的操作,并返回新值;

 type __sync_add_and_fetch (type *ptr, type value, ...)

 type __sync_sub_and_fetch (type *ptr, type value, ...)

 type __sync_or_and_fetch (type *ptr, type value, ...)

 type __sync_and_and_fetch (type *ptr, type value, ...)

 type __sync_xor_and_fetch (type *ptr, type value, ...)

 type __sync_nand_and_fetch (type *ptr, type value, ...)

 (3) 这些内建执行原子比较和交换。也就是说,如果*ptr的当前值是oldval,那么将newval写入*ptr。如果比较成功并且写入了newval,则"bool"版本将返回true。"val"版本在操作之前返回*ptr的内容。

 bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)

 type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)

 (4) 内存屏障

 __sync_synchronize (...)

  • 9
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值