您的位置:首页 > 编程语言 > Java开发

Java多线程编程-(16)-无锁CAS操作以及Java中Atomic并发包的“18罗汉”

2017-11-01 00:00 671 查看

一、背景

通过上面的学习,我们应该很清楚的知道了在多线程并发情况下如何保证数据的安全性和一致性的两种主要方法:一种是加锁,另一种是使用ThreadLocal。锁是一种以时间换空间的方式,而ThreadLocal是一种以空间换时间的方式
以上的内容一个是有锁操作,另一个是ThreadLocal的操作,那么是否有一种不使用锁就可以实现多线程的并发那?答案是有!下边我们一点点介绍什么是无锁,以及无锁的常用类。

二、无锁

我们知道在进行线程切换的时候是需要进行上下文切换的,意思就是在切换线程的时候会保存上一任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
上述说的上下文切换也就是我们说的线程切换的时候所花费的时间和资源开销。因此,如何减少上下文切换是一种可以提高多线程并发效率的有效方案。这里的无锁正是一种减少上下文切换的技术。
对于并发控制而言,锁是一种悲观的策略。它总是假设每一次的临界区操作会产生冲突,因此,必须对每次操作都小心翼翼。如果有多个线程同时需要访问临界区资源,就宁可牺牲性能让线程进行等待,所以说锁会阻塞线程执行。
而无锁是一种乐观的策略,它会假设对资源的访问是没有冲突的。既然没有冲突,自然不需要等待,所以所有的线程都可以在不停顿的状态下持续执行。
那遇到冲突怎么办呢?无锁的策略使用一种叫做比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止

三、什么是比较交换(CAS)

(1)与锁相比,使用比较交换(下文简称CAS)会使程序看起来更加复杂一些。但由于其非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。
(2)无锁的好处:
第一,在高并发的情况下,它比有锁的程序拥有更好的性能;
第二,它天生就是死锁免疫的。

就凭借这两个优势,就值得我们冒险尝试使用无锁的并发。
(3)CAS算法的过程是这样:它包含三个参数CAS(V,E,N): V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
(4)CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
(5)简单地说,CAS需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,那说明它已经被别人修改过了。你就重新读取,再次尝试修改就好了。
(6)在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令。在JDK 5.0以后,虚拟机便可以使用这个指令来实现并发操作和并发数据结构,并且,这种操作在虚拟机中可以说是无处不在。

四、Java中的原子操作类

Java中的原子操作类大致可以分为4类:原子更新基本类型、原子更新数组类型、原子更新引用类型、原子更新属性类型。这些原子类中都是用了无锁的概念,有的地方直接使用CAS操作的线程安全的类型。
JDK 1.7.9版本
java.util.concurrent.atomic
包如下:



分类如下:



五、原子更新基本类型

AtomicBoolean:原子更新布尔类型;

AtomicInteger:原子更新整数类型;

AtomicLong:原子更新长整型类型;

三个的基本原理大致一样,这里讨论AtomicInteger,方法和属性如下:



这个的每一个方法根据方法名可以了解其大致意思,不在这里赘述,看一个案例,产生10000个整数并输出:



接下来看一下
incrementAndGet()
 这个方法的实现:




首先获取当前的值,这里的get方法调用结果返回一个volatile 修饰的value值,这样的话,上面正在访问的线程可以发现其他线程对临界区数据的修改,volatile实现了JMM中的可见性。使得对临界区资源的修改可以马上被其他线程看到。
int next = current + 1;
这一行代码得到的结果就是需要更新的值,也就是需要对原来的值进行加1操作。
if (compareAndSet(current, next)) { 
          return next;
       }
这一行代码就是调用了CAS方法进行原子更新操作的,符合CAS的设计原理,意思就是在设置值的时候,首先判断一下是否和预期的值一样,如果一样则修改,不一样的话就表示修改失败,而这里最外层是
for (;;)
 也就是一个死循环,这是因为在CAS无锁的情况下我们的修改可能会失败,这样的话通过这个死循环就可以继续循环知道成功修改位置,这也是实现CAS的关键。



这里的
compareAndSet()
 方法调用的是Unsafe的
compareAndSwapInt()
 方法,Unsafe类是CAS实现的核心。
从名字可知,这个类标记为不安全的,它本质上可以理解为是Java中的指针,Unsafe封装了一下不安全的操作,这是因为指针是不安全的,不正确的使用可能会造成意想不到的结果,因此JDK作者不希望用户使用这个类,只可以在JDK内部使用到。Atomic包里的类基本都是使用Unsafe这个类实现的
Unsafe提供了3种CAS方法,具体方法如下,难以理解,这里只展示一下,不做过多解释:



六、原子更新引用类型

AtomicReference:原子更新引用类型;

AtomicStampedReference:原子更新带有版本号的引用类型;

AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记为和引用类型;

(1)AtomicReference
AtomicReference是对普通的对象的引用,可以保证我们在修改对象应用的时候保证线程的安全性,举例如下:



这是一个简单的使用,但是有一个情况是需要注意的,因为在每次
compareAndSet
 的时候,假如我们预期的值被别的线程修改了,然后在又被其他线程修改会原来的状态了,如下图:



他不像操作AtomicInteger等一样,即使中间被修改,但是他是没有状态的,最后的记过不会受到影响,道理很简单,就是我们数学中的等式替换,但是对于
AtomicReference
 这种状态的迁移可能是一种灾难!
(2)表示AtomicReference状态的实例
假设有一家咖啡店,为每一位会员卡余额小于20的会员一次性充值20元,以刺激消费。条件是只充值一次!





执行结果:



可以看出在账户充值的时候,会员可能正在消费,由于在充值的时候,判断的是账户余额是否小于20,如果是则进行充值,但是没有考虑到如何只充值一次的情况,因为他只是比较预期的值是否小于20,而无法判断该值的状态,所以账户被多次充值了,这就是因为AtomicReference无法表达状态的迁移!
(3)AtomicStampedReference带有时间戳的对象引用类型
为了表述一个有状态迁移的
AtomicReference
而升级为带有时间戳的对象引用
AtomicStampedReference
AtomicStampedReference
 解决了上述对象在修改过程中,丢失状态信息的问题,使得对象的值不仅与预期的值相比较,还通过时间戳进行比较,这就可以很好的解决对象被反复修改导致线程无法正确判断对象状态的问题。
AtomicStampedReference
 更新值的时候还必须要更新时间戳,只有当值满足预期且时间戳满足预期的时候,写才会成功!
把上述的代码改成使用
AtomicStampedReference
 的方式如下:





执行结果:



可以看出只充值一次!

七、原子更新数组类型

AtomicIntegerArray:原子更新整数型数组里的元素;

AtomicLongArray:原子更新长整型数组里的元素;

AtomicReferenceArray:原子更新引用类型数组里的元素;

简单实例:



八、原子更新属性类型

如果需要原子地更新某个类里的某个字段时,就需要使用原子更新字段值,主要有下边三个:

AtomicIntegerFieldUpdater:原子更新整数型字段;

AtomicLongFieldUpdater:原子更新长整型字段;

AtomicReferenceFieldUpdater:原子更新引用类型里的字段;

示例如下:



参考文章:
1、http://www.cnblogs.com/756623607-zhang/p/6876060.html

注:查看源代码请点击阅读原文,PC端效果更佳!



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: