原子操作的原理和实现

目录

相关术语

处理器如何实现原子操作

Java如何实现原子操作

循环CAS实现原子操作

使用锁机制实现原子操作


原子操作是指一个或者多个不可再分割的操作。这些操作的执行顺序不能被打乱。

相关术语

缓存行:缓存的最小操作单位

(面试题、重点)比较并且交换(CAS):CAS操作(CAS无锁状态下安全更新数据)需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。我们一个线程往回更新的时候会锁总线,其他线程即使切换到也不能传回数据,所有可以在多线程下保证数据的正确性(这个表明我们不能自己写一个cas,需要调用Java核心的Java方法来使用操作系统的cas)

cas有个问题,对中间的感知不清楚(ABA问题)

CPU流水线:每个人只做一件事,没有切换时间,提高总体效率(假如共10个任务,每个任务5个基本指令,共50个基本指令。流水线就会先将所有任务中的相同的基本指令一起执行完之后,再更换线路来执行其他相同的指令,这样减少了线路的切换,而线路切换需要的时间很长)(流水线可能导致顺序错乱问题,在同一个线程中,代码的底层执行顺序可能是乱的,可能执行顺序是先第二行再第一行)

内存顺序冲突:内存顺序冲突一般是由假共享引起的,假共享是指多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现这个内存顺序冲突时,CPU必须清空流水线

处理器如何实现原子操作

处理器通过总线锁和缓存锁的方式来实现原子操作

总线锁:使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住

缓存锁:对应一个内存的不同线程的高速缓存,在操作一个时,其他缓存行全部锁住

跨总线、跨缓存行、跨页:

cpu和内存之间的总线宽度大约36~41位(36~41根导线),单个导线同一时刻传一个电信号;一个缓存行64比特,一个内存页4kb,跨总线、跨缓存行、跨页指的是数据的大小超过单个大小(跨导线可能是long类型的多于41位;缓存行可能是一个批次要处理的数据,不是一个变量;页可能是一个超过4kb大小的综合数据)

锁住总线所有的线程都不能返回,锁缓存与缓存相关的不能返回,但缓存锁有些处理器不支持

Java如何实现原子操作

在Java中可以通过锁和循环CAS的方式来实现原子操作。

循环CAS实现原子操作

JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。Java本身实现不了CAS操作,必须依托于操作系统内核

CAS概述

CAS的全称是 Compare-and-Swap,也就是比较并交换,是并发编程中一种常用的算法。它包含了三个参数:V,A,B。其中,V表示要读写的内存位置,A表示旧的预期值,B表示新值。CAS指令执行时,当且仅当V的值等于预期值A时,才会将V的值设为B,如果V和A不同,说明可能是其他线程做了更新,那么当前线程就什么都不做,最后,CAS返回的是V的真实值。而在多线程的情况下,当多个线程同时使用CAS操作一个变量时,只有一个会成功并更新值,其余线程均会失败,但失败的线程不会被挂起,而是不断的再次循环重试。

从Java 1.5开始,JDK的并发包里提供了一些类来支持原子操作,如

AtomicBoolean(用原子方式更新的boolean值)AtomicInteger(用原子方式更新的int值)AtomicLong(用原子方式更新的long值)

线程安全,在多线程操作时,不需要我们手动加锁和各种同步控制,自己内部已经加了。

这些原子包装类还提供了有用的工具方法,比如以原子的方式将当前值自增1和自减1。

调用方法实现cas

原子操作的原理和实现_第1张图片

(Java中的cas不仅存在于AtomicInteger一个类中,其他类也可以有)

CAS实现原子操作的三大问题

1.(面试题)ABA问题

如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。cas没有发现的话还会有线程根据最开始的A认为和旧值相同而接着返回

解决方法:加版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。

从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference,这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2.循环时间长开销大,自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销

3.只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。也可以将多个共享变量变为一个变量(例:ij = 2a)

cas不加锁可以在多线程下保证了计算的正确率,但是在 高并发情况下,会有很多线程在不符合时重新计算(假如有10000个线程进行计算,一个返回后其余9999都需要重新获取数据计算),很大的损耗了cpu的性能。低并发可以,高并发性能不好

锁里面释放锁获取锁都是cas,是因为重量级锁获取锁的时候用cas获取,一旦失败的话停止cas,进入阻塞队列。不是无限的cas

使用锁机制实现原子操作

锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。实现锁的方式都用了循环CAS,即当一个线程想进入

你可能感兴趣的:(并发编程,java,spring,面试)