本文共 4359 字,大约阅读时间需要 14 分钟。
原子操作:一个或多个操作在CPU执行过程中不被中断的特性
当我们说原子操作时,需要分清楚针对的是CPU指令级别还是高级语言级别。
比如:经典的银行转账场景,是语言级别的原子操作;
而当我们说volatile修饰的变量的复合操作,其原子性不能被保证,指的是CPU指令级别。 二者的本质是一致的。“原子操作”的实质其实并不是指“不可分割”,这只是外在表现,本质在于多个资源之间有一致性的要求,操作的中间态对外不可见。
比如:在32位机器上写64位的long变量有中间状态(只写了64位中的32位);银行转账操作中也有中间状态(A向B转账,A扣钱了,B还没来得及加钱)
Java使用锁和自旋CAS实现原子操作
public class Counter { private final AtomicInteger atomicI = new AtomicInteger(0); private int i = 0; public static void main(String[] args) { Counter counter = new Counter(); ArrayListlist = new ArrayList<>(1000); long start = System.currentTimeMillis(); IntStream.range(0, 100).forEach(u -> { list.add(new Thread(() -> IntStream.range(0, 1000).forEach(v -> { counter.safeCount(); counter.count(); }))); }); list.forEach(Thread::start); /* wait for all the threads to complete*/ list.forEach(u -> { try { u.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(counter.i); System.out.println(counter.atomicI.get()); System.out.println(System.currentTimeMillis() - start); } /* 使用CAS 来实现原子操作*/ public void safeCount() { for (; ; ) { int i = atomicI.get(); /*Atomically sets the value to the given updated value if the current value == the expected value.*/ /*Parameters: expect - the expected value update - the new value*/ /* 其实,假如使用 原子类来实现计数器,不需要直接用 cas 的API,原子类已经提供了现成的API了*/ boolean success = atomicI.compareAndSet(i, i + 1); if (success) { break; } } } /* 使用 锁 来实现原子操作*/ public synchronized void safeCount1() { i++; } /* 线程不安全的累加*/ public void count() { i++; }}
并发包中提供了很多原子类来支持原子操作:
CAS是并发包的基石,但用CAS有三个问题:
1)ABA问题
根源:CAS的本质是对变量的current value
,期望值expected value
进行比较,二者相等时,再将 给定值given update value
设为当前值。 因此会存在一种场景,变量值原来是A,变成了B,又变成了A,使用CAS检查时会发现值并未变化,实际上是变化了。
对于数值类型的变量,比如int,这种问题关系不大,但对于引用类型,则会产生很大影响。ABA问题解决思路:版本号。在变量前加版本号,每次变量更新时将版本号加1,A -> B -> A,就变成 1A -> 2B -> 3A。
JDK5之后Atomic包中提供了AtomicStampedReference#compareAndSet来解决ABA问题。public boolean compareAndSet(@Nullable V expectedReference, V newReference, int expectedStamp, int newStamp)Atomically sets the value of both the reference and stamp to the given update values if the current reference is == to the expected reference and the current stamp is equal to the expected stamp.Parameters:expectedReference - the expected value of the referencenewReference - the new value for the referenceexpectedStamp - the expected value of the stampnewStamp - the new value for the stamp
2)循环时间长则开销大
自旋CAS若长时间不成功,会对CPU造成较大开销。不过有的JVM可支持CPU的pause指令的话,效率可有一定提升。pause作用:
memorey order violation
)引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。3)只能保证一个共享变量的原子操作
CAS只能对单个共享变量如是操作,对多个共享变量操作时则无法保证原子性,此时可以用锁。另外,也可“取巧”,将多个共享变量合成一个共享变量来操作。比如a=2,b=t,合并起来ab=2t,然后用CAS操作ab.
JDK5提供AtomicReference
保证引用对象间的原子性,它可将多个变量放在一个对象中来进行CAS操作。
锁机制保证只有拿到锁的线程才能操作锁定的内存区域。
JVM内部实现了多种锁,偏向锁、轻量锁、互斥锁。不过轻量锁、互斥锁(即不包括偏向锁),实现锁时还是使用了CAS,即:一个线程进入同步代码时用自CAS拿锁,退出块的时候用CAS释放锁。synchronized
锁定的临界区代码对共享变量的操作是原子操作。 首先,CPU会自动保证基本的内存操作的原子性。CPU保证从内存中读写一个字节是原子的,即:当一个CPU读一个字节时,其他处理器不能访问这个字节的内存地址。
但对于复杂的内存操作如跨总线跨度、跨多个缓存行的访问,CPU是不能自动保证的。不过,CPU提供总线锁定和缓存锁定。假如多个处理器同时读改写共享变量,这种操作(e.g. i++)不是原子的,操作完的共享变量的值会和期望的不一致。
原因:多个处理器同时从各自缓存读i,分别 + 1,分别写入内存。要想保证读改写共享变量的原子性,必须保证CPU1读改写该变量时,CPU2不能操作缓存了该变量内存地址的缓存。
总线锁就是解决此问题的。
总线锁:利用LOCK#信号,当一个CPU在总线上输出此信号,其他CPU的请求会被阻塞,则该CPU可以独占共享内存。
同一时刻,其实只要保证对某个内存地址的操作是原子的即可,但总线锁定把CPU和内存间的通信锁住了。锁定期间,其他CPU不能操作其他内存地址的数据,所以总线锁定的开销比较大。目前CPU会在一些场景下使用缓存锁替代总线锁来优化。
频繁使用的内存会被缓存到L1、L2、L3高速cache中,原子操作可直接在高速cache中进行,不需要声明总线锁。
缓存锁是指:缓存一致性机制阻止同时修改由两个以上CPU缓存的内存区域数据,当其他CPU回写已被锁定的缓存行数据时,会使缓存行无效。看下图:i 是同时被CPU1和CPU2缓存的内存区域变量;CPU1 修改缓存行中 i 时使用缓存锁定,则CPU2 不能同时缓存 i 的缓存行。(i 的缓存行会失效)
转载地址:http://dknsn.baihongyu.com/