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

一、背景

通过上面的学习,我们应该很清楚的知道了在多线程并发情况下如何保证数据的安全性和一致性的两种主要方法:一种是加锁,另一种是使用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包如下:

分类如下:

五、原子更新基本类型

  1. AtomicBoolean:原子更新布尔类型;
  2. AtomicInteger:原子更新整数类型;
  3. 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方法,具体方法如下,难以理解,这里只展示一下,不做过多解释:

六、原子更新引用类型

  1. AtomicReference:原子更新引用类型;
  2. AtomicStampedReference:原子更新带有版本号的引用类型;
  3. AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记为和引用类型;

(1)AtomicReference

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

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

他不像操作AtomicInteger等一样,即使中间被修改,但是他是没有状态的,最后的记过不会受到影响,道理很简单,就是我们数学中的等式替换,但是对于AtomicReference 这种状态的迁移可能是一种灾难!

(2)表示AtomicReference状态的实例

假设有一家咖啡店,为每一位会员卡余额小于20的会员一次性充值20元,以刺激消费。条件是只充值一次!

执行结果:

可以看出在账户充值的时候,会员可能正在消费,由于在充值的时候,判断的是账户余额是否小于20,如果是则进行充值,但是没有考虑到如何只充值一次的情况,因为他只是比较预期的值是否小于20,而无法判断该值的状态,所以账户被多次充值了,这就是因为AtomicReference无法表达状态的迁移!

(3)AtomicStampedReference带有时间戳的对象引用类型

为了表述一个有状态迁移的AtomicReference而升级为带有时间戳的对象引用AtomicStampedReferenceAtomicStampedReference 解决了上述对象在修改过程中,丢失状态信息的问题,使得对象的值不仅与预期的值相比较,还通过时间戳进行比较,这就可以很好的解决对象被反复修改导致线程无法正确判断对象状态的问题。

AtomicStampedReference 更新值的时候还必须要更新时间戳,只有当值满足预期且时间戳满足预期的时候,写才会成功!

把上述的代码改成使用AtomicStampedReference 的方式如下:

执行结果:

可以看出只充值一次!

七、原子更新数组类型

  1. AtomicIntegerArray:原子更新整数型数组里的元素;
  2. AtomicLongArray:原子更新长整型数组里的元素;
  3. AtomicReferenceArray:原子更新引用类型数组里的元素;

简单实例:

八、原子更新属性类型

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

  1. AtomicIntegerFieldUpdater:原子更新整数型字段;
  2. AtomicLongFieldUpdater:原子更新长整型字段;
  3. AtomicReferenceFieldUpdater:原子更新引用类型里的字段;

示例如下:

参考文章:

1、http://www.cnblogs.com/756623607-zhang/p/6876060.html

原文发布于微信公众号 - Java后端技术(JavaITWork)

原文发表时间:2017-11-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏IT杂记

Java Socket Timeout总结

1. Socket timeout     Java socket有如下两种timeout: 建立连接timeout,暂时就叫 connect timeout;...

6339
来自专栏Android开发实战

Nuwa学习笔记

With this Nuwa project,you can also have the repairing power, fix your android a...

1232
来自专栏博客园迁移

Java并发问题--乐观锁与悲观锁以及乐观锁的一种实现方式-CAS

  悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库...

1172
来自专栏coder修行路

Beego 框架学习(一)

 Beego官网本身已经整理的非常详细了,但是作为一个学习者,我还是决定自己好好整理一下,这样在后面使用的时候自己对每部分才能非常熟悉,即使忘记了,也可以迅速定...

5328
来自专栏抠抠空间

configparser模块

模块简介 该模块适用于配置文件的格式与windows ini文件类似,可以包含一个或多个节(section),每个节可以有多个参数(键=值)。 创建文件 来看...

2786
来自专栏python3

python3--基础总练习题

3、利用 python 打印前一天的本地时间,格式为‘2018-01-30’(面试题)

3863
来自专栏java 成神之路

并发编程基础知识点

3516
来自专栏Play & Scala 技术分享

HTTP Cookie的域名和路径匹配

2725
来自专栏大内老A

WCF服务端运行时架构体系详解[上篇]

WCF的服务端架构体系又可以成为服务寄宿端架构体系。我们知道,对于一个基于某种类型的服务进行寄宿只需要使用到一个唯一的对象,那就是ServiceHost。甚至在...

1959
来自专栏Java架构沉思录

关于Java锁机制面试官会怎么问

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边...

1361

扫码关注云+社区

领取腾讯云代金券